반응형

<data> 태그를 사용하면 activity 로직부분에서 구현하지않더라도, xml 쪽에서 바로 데이터바인딩이 가능하다

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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"
    tools:context=".MainActivity">

    <data>
        <import type="android.view.View"/>
        
        <variable
            name="isDebug"
            type="Boolean" />
    </data>
    
</layout>

 

<data> 태그 내부에 <import> 태그를 우선 살펴보자

 

import 태그란 ?

특정 클래스나 패키지를 임포트 할 수 있는데,

현재 View 를 임포트했기때문에 android:visibility, onClick 과 같은 속성 사용이 가능하다

 

//import 안했을 경우

android:visibility="@{isDebug ? View.VISIBLE : View.GONE}"

//import 했을 경우
android:visibility="@{isDebug ? VISIBLE : GONE}"

 

이런 차이로 보면 된다

 

variable 태그란 ?

단순 변수이다. 로직에서 값을 세팅하면 xml에서 사용이 가능하다

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(_binding.root)

    initView()
}

private fun initView() {
    _binding.isDebug = true
}

 

바인딩을 사용하는 경우의 예시이며, 데이터바인딩으로 바로 사용가능하다.

 

만약 type 으로 model 을 넣으면 모델 째로 사용이 가능하다.

...
    <data>
        <import type="android.view.View"/>

        <variable
            name="isDebug"
            type="Boolean" />

        <variable
            name="item"
            type="com.example.network_sample.model.DogResponse" />

    </data>

...

        <TextView
            android:text="@{item.status}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:ignore="MissingConstraints" />

 

 

github주소

 - showDog 프로젝트

https://github.com/Daseul727/Mobile-Skill-Up

728x90
반응형

 

이전 포스팅에서 api 통신을 하였는데, 결과값으로 이미지를 받을것이다.

해당 이미지를 뿌려주기 위한 activity를 꾸며보자

 

1. activity_main 수정

<?xml version="1.0" encoding="utf-8"?>
<layout 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"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <ImageView
            android:id="@+id/imgDog"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:contentDescription="@string/dog_image"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="30dp"
            android:layout_marginBottom="100dp"
            android:layout_marginStart="32dp"
            android:layout_marginEnd="32dp"/>

        <ProgressBar
            android:id="@+id/pbDog"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

        <ImageView
            android:id="@+id/imgRefresh"
            android:layout_width="50dp"
            android:layout_height="50dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintEnd_toEndOf="parent"
            android:src="@drawable/ic_refresh"
            android:contentDescription="@string/refresh"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/imgDog"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

이미지 소스는 적당한것을 붙여넣거나, 아래 깃주소에 해당 프로젝트 올려놨으니 참고!

https://github.com/Daseul727/mobile_sample/tree/main/network_mobile

 

mobile_sample/network_mobile at main · Daseul727/mobile_sample

코틀린 안드로이드 basic. Contribute to Daseul727/mobile_sample development by creating an account on GitHub.

github.com

 

2. data binding 설정

data binding이랑 이미지 로드 라이브러리를 사용하기 위해 build.gradle 을 수정해야한다

android {
    namespace 'com.example.network_sample'
    compileSdk 34

...
    //추가
    buildFeatures {
        viewBinding = true
        dataBinding true
    }
}

dependencies {

    ...
    //추가
    /*image load*/
    implementation "io.coil-kt:coil:$coilVersion"
}

 

coilVersion 은 1.2.2 를 사용했다

 

 

3. MainActivity 수정

 

1) view data binding

우선 activity_main과 연결하기위해 바인딩을 시켜준다

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainViewModel: MainViewModel
    
    //추가
    private lateinit var _binding: ActivityMainBinding 

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        //추가
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(_binding.root)
    }

 

위에서 build.gradle에 데이터바인딩 사용설정이 성공적으로 완료되었다면 ActivityMainBinding 이 자동완성으로 import 될것이다.

 

그리고 onCreate 에서 한번더 _binding을 inflate 시켜주면 초기화 완료

 

2) api 연결

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainViewModel: MainViewModel
    private lateinit var _binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(_binding.root)
    }

    //추가
    private fun fetchResponse() {
        mainViewModel.fetchDogResponse()
        _binding.pbDog.visibility = View.VISIBLE
    }

 

이전 포스팅에서 model에 만들었던 함수를 호출하자

 

3) observer 추가

이제 실제로 fetchresponse 를 호출해볼건데, 해당함수 호출되어 model 의 "response"에 값이 담기면,

담기는것을 observe 하고있다가 화면에 데이터를 뿌려주는 함수를 넣어준다.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainViewModel: MainViewModel
    private lateinit var _binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(_binding.root)
        
        //추가
        fetchData()
        //추가
        _binding.imgRefresh.setOnClickListener {
            fetchResponse()
        }
    }

    //추가
    private fun fetchData() {
        fetchResponse()

        mainViewModel.response.observe(this) { res->
            when (res) {
                is NetworkResult.Success -> {
                    res.data?.let {
                        _binding.imgDog.load(
                            res.data.message
                        ) {
                            transformations(RoundedCornersTransformation(16f))
                        }
                    }
                    _binding.pbDog.visibility = View.GONE
                }
                is NetworkResult.Error -> {
                    _binding.pbDog.visibility = View.GONE
                    Toast.makeText(
                        this,
                        res.message,
                        Toast.LENGTH_SHORT
                    ).show()
                }
                is NetworkResult.Loading -> {
                    _binding.pbDog.visibility = View.VISIBLE
                }
            }
        }
    }

    private fun fetchResponse() {
        mainViewModel.fetchDogResponse()
        _binding.pbDog.visibility = View.VISIBLE
    }
}

 

이렇게하면 완성!

 

 

github주소

 - showDog 프로젝트

https://github.com/Daseul727/Mobile-Skill-Up

728x90
반응형

 

1. Network 통신설정

Network 통신하여 강아지 이미지를 받아보겠음!

 

1) Manifest network 권한추가

http 통신을 하기 위하여 Retrofit 이라는 라이브러리를 사용할것이다.

해당 라이브러리를 사용하기에 앞서 아래의 퍼미션을 추가하자

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

//추가
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    
    <application
        android:name=".NetworkApplication"
 ...

 

2) build.gradle 설정 추가

Retrofit 관련 설정을 build.gradle(app)에 추가한다

/* Retrofit */
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

/*lifecycle*/
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"

 

3) 통신관련 정보 static 보관

통신과 관련된 URL 이나 IP정보는 변하지않기때문에 한곳에서 관리하는것이 좋다.

 

utils 폴더 하나만들어서, Constants 파일을 만들어주자

package com.maxi.dogapi.utils

class Constants {

    companion object {

        const val BASE_URL = "https://dog.ceo/"
        const val RANDOM_URL = "api/breeds/image/random"
    }
}

 

companion object를 쓰면 Constants 인스턴스를 생성하지않고도 BASE_URL 에 접근할수있다

 

2. 통신연결

위에서 설정한 RANDOM_URL과 get방식 통신을 할건데, 결과값을 받기위한 Response 클래스를 만들어주자

 

1) Response class 생성

package com.example.network_sample.model

import com.google.gson.annotations.SerializedName

data class DogResponse(
    @SerializedName("message")
    val message: String,
    @SerializedName("status")
    val status: String
)

 

data class 를 사용하면 클래스의 기본적인 메소드들(toString(), equals(), hashCode(), copy())을 자동으로 생성해준다.

 

2) API Service 생성

package com.example.network_sample.data.remote

import com.example.network_sample.model.DogResponse
import com.example.network_sample.utils.Constants
import retrofit2.Response
import retrofit2.http.GET

interface APIService {

    @GET(Constants.RANDOM_URL)
    suspend fun getDog(): Response<DogResponse>
}

 

만약 @GET import 안되면 retrofit build.gradle 설정을 확인해보자.

위에서 RANDOM_URL 을 만든이유는, 저런식으로 상수화 시켜서 사용하기 위함이었다.

 

실제로 통신할때는 해당 api를 바로 DI 시켜서 쓰지않는다. 별도 ServiceImpl 같이 하나 만들어서 주입해주자

 

3) RemoteDataSource 생성

class RemoteDataSource @Inject constructor(private val dogService: APIService) {

    suspend fun getDog() =
        dogService.getDog()

}

 

@Inject?

이전 포스팅에서는 @Inject 로 DI 를 할수있다고 했는데 @Inject 방법에는 두가지가 있다.

 

방법1. 필드주입

이전 포스팅과 같이 class 내부에 @Inject 하는방법이며 코드가 간결하게보일 수 있지만 외부 의존성에 대한 테스트가 어려울수 있다고 한다

 

방법2. 생성자주입

클래스 의존성을 외부로부터 완벽히 분리하고 의존성주입을 명시적으로 만든다.

왠만하면 생상자주입이 권장되는편인가보다.

 

constructor ?

클래스 생성자 선언하는 방법중에 하나인데, 파라미터 선언없이도 바로 생성이 가능하게 만들어준다

// 클래스의 주 생성자를 선언하는 방법
class MyClass constructor(param1: Int, param2: String) {
    // 클래스 본문
}

// 클래스의 주 생성자를 선언하지 않고 속성과 함께 선언하는 방법
class MyClass(param1: Int, param2: String) {
    // 주 생성자 파라미터로 선언된 속성들
    val property1: Int = param1
    val property2: String = param2
}

 

suspend?


코틀린에서 코루틴(coroutine)을 사용하는 함수를 정의할 때 사용되는 키워드
네트워크 요청이나 파일 I/O 등의 비동기적인 작업을 수행할 수 있고 위 코드를 보면 getDog() 함수는 일시 중단되어 네트워크 응답을 기다린 후 결과를 반환한다.

코루틴은 Android에서 비동기적인 작업을 수행할 때 매우 유용하며, UI 스레드를 차단하지 않고 작업을 수행할 수 있도록 해준다고 한다.

 

-> 결과적으론 RemoteDataSource 에서 API 를 비동기연결하였다.

 

 

3. Model 과 API 연결

 

이제 실제로 MainViewModel 에서 직접 연결할 repository 를 만들어보자.

 

model  ---- RemoteDataSource 를 연결하는 공간이라고 보면 된다.

 

model에게 api 통신결과를 보내줄건데, success/fail 에 따라 어떻게 보내줄건지도 정의해야할것이다.

repository 를 생성하기에 앞서, BaseApiResponse를 만들어보자

 

1) BaseApiResponse 생성

package com.maxi.dogapi.model

import com.maxi.dogapi.utils.NetworkResult
import retrofit2.Response

abstract class BaseApiResponse {

    suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResult<T> {
        try {
            val response = apiCall()
            if (response.isSuccessful) {
                val body = response.body()
                body?.let {
                    return NetworkResult.Success(body)
                }
            }
            return error("${response.code()} ${response.message()}")
        } catch (e: Exception) {
            return error(e.message ?: e.toString())
        }
    }

    private fun <T> error(errorMessage: String): NetworkResult<T> =
        NetworkResult.Error("Api call failed $errorMessage")

}

 

이런식으로 api 통신결과를 어떻게 처리할지 간단히 정의해하였고 result 를 보내기위해 NetworkResult 도 아래와 같이 만들어주었다

package com.example.network_sample.utils

sealed class NetworkResult<T>(
    val data: T? = null,
    val message: String? = null
) {

    class Success<T>(data: T) : NetworkResult<T>(data)

    class Error<T>(message: String, data: T? = null) : NetworkResult<T>(data, message)

    class Loading<T> : NetworkResult<T>()

}

 

2) Repository 생성

위에서 만든 BaseApiResponse를 사용하여 Repository를 만들어보자

 

package com.example.network_sample.data

import com.example.network_sample.data.remote.RemoteDataSource
import com.example.network_sample.model.BaseApiResponse
import com.example.network_sample.model.DogResponse
import com.example.network_sample.utils.NetworkResult
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

@ActivityRetainedScoped
class Repository @Inject constructor(
    private val remoteDataSource: RemoteDataSource
): BaseApiResponse() {

    suspend fun getDog(): Flow<NetworkResult<DogResponse>> {
        return flow { 
            emit(safeApiCall { remoteDataSource.getDog() })
        }.flowOn(Dispatchers.IO)
    }

}

 

 

4) Model - Repository 연결

 

위에서 만든 repository 를 model에서 불러보자

package com.example.network_sample.model

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.network_sample.data.Repository
import com.example.network_sample.utils.NetworkResult
import kotlinx.coroutines.launch
import javax.inject.Inject

class MainViewModel @Inject constructor(private val repository: Repository) : ViewModel() {

    private val _response : MutableLiveData<NetworkResult<DogResponse>> = MutableLiveData()
    val response : LiveData<NetworkResult<DogResponse>> = _response

    fun fetchDogResponse() = viewModelScope.launch {
        repository.getDog().collect { values ->
            _response.value = values
        }
    }
}

 

만약 viewModelScope가 안써지진다면, 상단 build.gradle 설정에 lifecycle이 제대로 설정되어잇는지 확인해보자

 

이렇게하면 fetchDogResponse 를 호출하면 결과값이 잘 호출될것이다

 

 

github주소

 - showDog 프로젝트

https://github.com/Daseul727/Mobile-Skill-Up

728x90
반응형

Hilt 란?

의존성주입(DI)를 위한 라이브러리이다.

 

나는 자바개발자로 시작했기 때문에 Spring boot 를 예로 들겠다.

spring boot 에서 @SpringBootApplication 로, 스프링부트 시작점이라는것을 알수있고

의존성 주입, 다양한 어노테이션을 사용할수있다.

 

1. 새 프로젝트 생성

 

안드로이드 스튜디오에서 New project 해주었다

 

 

안드로이드에서도 스프링처럼 어노테이션을 활용할수있는데, 그러려면 hilt 설정을 build.gradle에 추가해줘야한다.

 

2. Hilt 추가하기

// gradle(project)
buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.50'
    }
}

// gradle(app)
plugins {
    id 'dagger.hilt.android.plugin'
    id 'kotlin-kapt'
}
dependencies {
    implementation "com.google.dagger:hilt-android:2.50"
    kapt "com.google.dagger:hilt-android-compiler:2.50"
}

 

혹시 Unsupported metadata version. Check that your Kotlin version is >= 1.0 에러가 발생한다면, 

 

https://dagger.dev/hilt/gradle-setup

 

Gradle Build Setup

Hilt dependencies To use Hilt, add the following build dependencies to the Android Gradle module’s build.gradle file: dependencies { implementation 'com.google.dagger:hilt-android:2.51' annotationProcessor 'com.google.dagger:hilt-compiler:2.51' // For in

dagger.dev

 

이곳에서 최신버전을 확인해서 적용시켜주자

 

3. Application 클래스 추가

MainActivity가 위치한 폴더에 자기가 원하는 이름의 코틀린 클래스를 추가해준다.

package com.example.network_sample

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class NetworkApplication : Application(){
}

 

 

@HiltAndroidApp 어노테이션을 사용함으로써 의존성 주입이 가능해졌다.

지금은 아무 내용도 입력하지않을것이지만, 나중에는 전역변수 등 공통으로 들어가는 로직을 추가할 수 있다.

 

Manifest 쪽에도 추가해주자

...

<application
    android:name=".NetworkApplication"
    android:allowBackup="true"
    ....

 

4. Activity 에 의존성 추가

mainViewModel 이라는 model을 만들어서 MainActivity에 의존성주입해보자

package com.example.network_sample.model

import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {

}

 

해당 클래스를 의존성주입하려면 2가지 방법이 있다.

 

방법 1. @Inject 사용

직접 구현한 클래스에 사용가능하다

 

방법 2. @Module 사용

인터페이스, 추상 클래스, 직접 구현불가한 클래스 에 사용 가능하다.

위 클래스들은 @Inject 사용이 불가하므로 @Module 사용한다.

 

나는 직접구현한 클래스이므로, @Inject를 사용하겠다.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainViewModel: MainViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

 

DI를 성공적으로 진행하기위해서, MainActivity에 @AndroidEntryPoint 어노테이션을 붙여준다.

 

@AndroidEntryPoint란?

Hilt가 애플리케이션의 컴포넌트를 생성/주입할수있게된다.

 

안쓰면? 

해당 클래스를 주입 가능한 대상으로 인식하지 않아서 필드에 의존성을 주입할 수 없다.

 

아래 유형에서 @AndroidEntryPoint  사용가능하다

(Application, Activity, Fragment, service, View, BroadcastReceiver, ViewModel)

 

 

 

github주소

 - showDog 프로젝트

https://github.com/Daseul727/Mobile-Skill-Up

728x90

+ Recent posts