반응형

fragment 를 이동하는 방법에는 두가지가 있다.

 

navication component 를 사용하는것과 사용하지 않는것.

 

1~2 페이지만 있다면 일반

뒤로가기, 딥링크, 애니메이션 등 효과를 줘야한다면 navigation component 사용하면 된다

 

1. Navigation graph 설정

네비게이션 컴포넌트를 사용하려면 navigation graph 를 추가해야한다.

그전에 우선 build.gradle 을 수정하자

 

1) build.gradle 수정

dependencies {

...
    def nav_version = "2.4.1"

    // navigation
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

 

2) Main Fragment 생성

Main Activity 에 띄울 mainFragment 를 생성한다

fragment_main.xml 과 MainFragment.kt 파일을 각각  생성하자

 

build.gradle 데이터바인딩 설정 추가

android {
...
    dataBinding {
        enabled = true
    }
}

dependencies {
...

 

fragment_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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white">

        <EditText
            android:id="@+id/et_message"
            android:layout_width="144dp"
            android:layout_height="35dp"
            android:background="#ffdddd"
            app:layout_constraintBottom_toTopOf="@id/btn_move_second"
            app:layout_constraintEnd_toEndOf="@id/btn_move_second"
            app:layout_constraintStart_toStartOf="@id/btn_move_second" />

        <Button
            android:id="@+id/btn_move_second"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:text="go second"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btn_move_third"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:text="go Third"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/btn_move_second" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

MainFragment 추가

class MainFragment : Fragment() {

    private var _binding: FragmentMainBinding? = null
    private val binding: FragmentMainBinding
        get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = DataBindingUtil.inflate(
            layoutInflater, R.layout.fragment_main, container, false
        )

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            btnMoveSecond.setOnClickListener {

            }

            btnMoveThird.setOnClickListener {

            }
        }
    }

}

 

3) navigation graph 추가

res 폴더에 navigation 패키지를 생성하고 navigation_graph.xml 파일을 생성한다

<?xml version="1.0" encoding="utf-8"?>
<navigation 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/navigation_graph"
    app:startDestination="@id/mainFragment">

    <fragment
        android:id="@+id/mainFragment"
        android:name="com.example.movepage.MainFragment"
        android:label="MainFragment">
    </fragment>


</navigation>

 

startDestination : 제일 먼저 실행 할 fragment 를 설정한다
fragment : 기본적으로 보여줄 frament 를 넣어준다. 해당공간에 앞으로 이동될 fragment 들이 위치할것

 

2. Fragment 이동

상단의 title bar 에서 뒤로가기, fragment 에서 다른 fragment 로 이동하기를 해보자

 

1) title bar 생성

적당히 타이틀, 뒤로가기버튼을 넣어줬다

<?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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/title_background"
        tools:context=".SecondFragment">

        <ImageButton
            android:id="@+id/btn_back"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginStart="20dp"
            android:background="@drawable/btn_back"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/txt_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="title"
            android:textSize="18dp"
            android:textColor="@color/black"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

 

2) Second Fragment , Thid Fragment 추가

첫번째로 이동할 페이지를 만들어준다

 

title 추가 해주고, 다른 fragment 로 이동할 버튼하나 넣어준다

<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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/teal_200"
        tools:context=".SecondFragment">

        <include
            android:id="@+id/inc_titlebar"
            android:layout_height="50dp"
            android:layout_width="match_parent"
            layout="@layout/inc_title_bar" />

        <Button
            android:id="@+id/btn_move"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:text="go third"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

데이터바인딩 로직추가

class SecondFragment : Fragment() {

    private var _binding : FragmentSecondBinding? = null
    private val binding: FragmentSecondBinding
        get() = _binding!!

    companion object {
        fun newInstance() : SecondFragment {
            val fragment = SecondFragment()
            val args = Bundle()
            fragment.arguments = args
            return fragment
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_second,container,false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            incTitlebar.txtTitle.text = "Second"
            incTitlebar.btnBack.setOnClickListener {
                
            }
            
            btnMove.setOnClickListener {
                
            }
        }
    }
}

 

이런식으로 Third Fragment 도 생성해준다.

 

그리고 타이틀의 뒤로가기 버튼과 화면 페이지이동 버튼에 onclick 이벤트를 줄건데

다수의 fragment 에서 공통으로 fragment 이동로직을 사용해야하므로, 해당로직은 MainActivity 에 넣기로 하자

 

4) Main Activity 에 공통 fragment 이동 로직 추가

MainActivity 위에 다양한 fragment 가 올라갈것이고,

모든 fragment 들이 공통적으로 페이지이동 로직이 필요하기때문에, MainActivity 에 fragment 이동 공통로직을 작성한다

 

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

	//추가
	fun openFragment(fragment: Fragment, tag: String) {
        val transaction: FragmentTransaction = supportFragmentManager.beginTransaction()
        transaction.replace(R.id.nav_host_fragment, fragment)
        transaction.addToBackStack(tag)
        transaction.commit()
    }

}

 

 

해당 로직을 MainFragment, SecondFragment 에서 사용하자

 

MainFragment

btnMoveSecond.setOnClickListener {
    val act = activity as MainActivity
    act.openFragment(ThirdFragment() , "2")
}
btnMoveThird.setOnClickListener {
    val act = activity as MainActivity
    act.openFragment(ThirdFragment() , "3")
}

 

내가 변경하고싶은 fragment 와 해당 fragment 임을 특정할수있는 TAG를 끼워준다

 

Second Fragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    //추가
    binding.apply {
        incTitlebar.txtTitle.text = "Second"

        incTitlebar.btnBack.setOnClickListener {
        }

        btnMove.setOnClickListener {
            val act = activity as MainActivity
            act.openFragment(ThirdFragment(), "3")
        }
    }
}

 

title 의 text 를 주고 onclick 이벤트를 작성했다.

 

이렇게 MainFragment 의 로직을 통해서 fragment 이동이 가능하다

 

3. 뒤로가기

title bar 에서 공통으로 사용될 뒤로가기 로직을 MainActivity 에 작성한다

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun navigateUp(tag: String) {
        Log.d("TLOG", "btn back 2.................")
        supportFragmentManager.popBackStack(tag, FragmentManager.POP_BACK_STACK_INCLUSIVE)
    }
    
    ...

 

supportFargmentManager.popbackStack 을 이용하는데, 이때 파라미터로 tag 값이 필요하다

그리고 POP_BACK_STACK_INCLUSIVE 옵션은 백스택에서 해당 Fragment와 위의 Fragment 들을 모두 제거한다

 

이것을 활용하여 뒤로가기를 실행해보자

 

Second Fragment와 Third fragment 에서 titlebar의 click이벤트를 설정한다

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.apply {

        //추가
        incTitlebar.txtTitle.text = "Second"
        incTitlebar.btnBack.setOnClickListener {
            val act = activity as MainActivity
            act.navigateUp("2")
        }

        btnMove.setOnClickListener {
            val act = activity as MainActivity
            act.openFragment(ThirdFragment(), "3")
        }
    }
}

 

이렇게하면 뒤로가기 완료

 

 

4. 홈으로 이동하기

모든 back Stack 을 전부 지우고 MainFragment 로 이동하는 로직이다

 

이것도 동일하게 MainActivity 에 로직을 추가한다

fun clearBackStack() {
    val fragmentManager: FragmentManager = supportFragmentManager
    fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}

 

기존에는 TAG 값을 넣고 해당 TAG 를 없앴지만, null 을 넣으면 모든 stack 을 없애준다

 

5. 데이터 전달하기

특정 fragment 에서 다른 fragment 로 데이터 전달하는 로직을 만들어보자

 

1) MainFragment 에 EditText 추가

...
        <include
            android:id="@+id/inc_titlebar"
            android:layout_height="50dp"
            android:layout_width="match_parent"
            layout="@layout/inc_title_bar" />

		//추가
        <EditText
            android:id="@+id/et_message"
            android:layout_width="144dp"
            android:layout_height="35dp"
            android:background="#ffdddd"
            app:layout_constraintBottom_toTopOf="@id/btn_move_second"
            app:layout_constraintEnd_toEndOf="@id/btn_move_second"
            app:layout_constraintStart_toStartOf="@id/btn_move_second" />

        <Button
            android:id="@+id/btn_move_second"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:text="go second"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
....

 

그리고 onViewCreated 에서 secondFragment 로 이동할대 해당 string 값을 전달해주자

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.apply {

        incTitlebar.btnBack.visibility = View.GONE
        incTitlebar.txtTitle.text = "MAIN"

        btnMoveSecond.setOnClickListener {
            val paramData = if(etMessage.text.isNotEmpty()) etMessage.text.toString() else "empty msg"
            val bundle = Bundle()
            bundle.putString("param", paramData)
            val act = activity as MainActivity
            act.openFragment(SecondFragment.newInstance().apply {
                arguments = bundle
            }, "2")
        }
...

 

editText 값이 empty 라면 empty msg 라는 string을 세팅하고 bundle을 만들어서 string 값울 붙인다.

그리고 SecondFragment 에서 newInstance 할때 해당 bundle을 가져갈수있도록 argments에 넣어준다

 

2) Second Fragment 에서 받기

class SecondFragment : Fragment() {

    private var _binding : FragmentSecondBinding? = null
    private val binding: FragmentSecondBinding
        get() = _binding!!

    var strParam = ""

    companion object {
        fun newInstance() : SecondFragment {
            val fragment = SecondFragment()
            val args = Bundle()
            fragment.arguments = args
            return fragment
        }
    }

    fun setJsonParam() {
        val bundle = arguments
        bundle?.let {
            strParam = it.getString("param", "").toString()
            Log.d("TLOG - SecondFragment", "받은값 : $strParam")
        }
    }

...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
		
        //추가
        setJsonParam()
        
        binding.apply {
...

 

newInstance 에서 MainFragment 에서 보낸 Bundle 을 argument 에 저장하고,

해당 arguments 를 풀어서 strParam에 저장했다

 

 

 

전체 소스코드는 아래 깃주의 "movePage" 프로젝트에 위치해있다

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

728x90
반응형

<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