반응형

 

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
반응형

 

햄버거 버튼 동작이벤트를 넣어보자.

 

1. GNB UI 생성

MainActivity 로 이어질 Home

별도 Activity 로 이어질 버튼을 넣어보겠다

<?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">
    <LinearLayout
        android:id="@+id/ll_menu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="0dp"
        android:layout_marginLeft="0dp"
        android:orientation="vertical"
        android:background="@color/white">

        <LinearLayout
            android:id="@+id/ll_item_1"
            android:layout_width="match_parent"
            android:layout_height="53dp"
            android:gravity="center_vertical"
            android:paddingLeft="40dp"
            android:paddingRight="20dp"
            android:layout_marginBottom="4dp"
            android:background="@color/home_menu_bg_light_gray"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/tv_item_1"
                android:layout_width="wrap_content"
                android:layout_height="53dp"
                android:layout_gravity="center_vertical"
                android:paddingLeft="0dp"
                android:gravity="center_vertical"
                android:text="Home"
                android:textSize="@dimen/sp_17"
                android:maxLines="1"
                android:textColor="@color/orange"
                android:textStyle="bold" />
            <View
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
            </View>
            <ImageView
                android:id="@+id/iv_orange_arrow1"
                android:layout_width="7dp"
                android:layout_height="12dp"
                android:layout_marginRight="0dp"
                android:background="@drawable/btn_detail_org"/>
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_item_2"
            android:layout_width="match_parent"
            android:layout_height="53dp"
            android:gravity="center_vertical"
            android:paddingLeft="40dp"
            android:paddingRight="20dp"
            android:layout_marginBottom="4dp"
            android:background="@color/home_menu_bg_light_gray"
            android:orientation="horizontal">
            <TextView
                android:id="@+id/tv_item_2"
                android:layout_width="wrap_content"
                android:layout_height="53dp"
                android:layout_gravity="center_vertical"
                android:paddingLeft="0dp"
                android:gravity="center_vertical"
                android:text="기본 Recycler View"
                android:textSize="@dimen/sp_17"
                android:maxLines="1"
                android:textColor="@color/orange"
                android:textStyle="bold" />
            <View
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">
            </View>
            <ImageView
                android:id="@+id/iv_orange_arrow2"
                android:layout_width="7dp"
                android:layout_height="12dp"
                android:layout_marginRight="0dp"
                android:background="@drawable/btn_detail_org"/>
        </LinearLayout>

    </LinearLayout>
</layout>

 

나는 이런식으로 추가해봤다.

메뉴가 몇개안되기때문에 LinearLayout을 사용했음!!

 

2. MainActivity 에 GNB 영역 추가

다른 액티비티에서는 사용하지않고 Main 에 들어왔을때만 보이면 되기때문에 MainActivity 에만 추가하자

<com.example.basic_mobile.ui.TitleBar
        android:id="@+id/inc_titlebar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/title_height"
        app:layout_constraintTop_toTopOf="parent"/>

////추가////
<com.google.android.material.navigation.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:orientation="vertical"
    android:visibility="gone"
    tools:ignore="MissingConstraints">

    <com.example.basic_mobile.ui.CustomDrawerMenu
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</com.google.android.material.navigation.NavigationView>

 

 

 

그런데!! 이렇게하면 햄버거버튼 위에 위치하며 버튼이랑 겹쳐진다

각 요소들을 나열해놓기만 하고 줄을 제대로 세우지 않았기때문에 이런식으로 먹혀버린다...

 

 

LinearLayout 으로 서열을 제대로 알려주자

 

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.example.basic_mobile.ui.TitleBar
            android:id="@+id/inc_titlebar"
            android:layout_width="match_parent"
            android:layout_height="@dimen/title_height" />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/nav_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:orientation="vertical"
            android:visibility="gone">

            <com.example.basic_mobile.ui.CustomDrawerMenu
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </com.google.android.material.navigation.NavigationView>
        <Button
            android:id="@+id/btn_recycler"
            android:text="기본 Recycler View"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_epoxy"
            android:text="epoxy View"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/btn_recycler"/>
    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

 

 

이런식으로 ConstraintLayout -> LinearLayout -> 요소 배치 의 방식으로 설정하면 된다.

 

자 이제 보여줄 화면을 세팅해놨다면, 햄버거버튼 클릭시에만 NavigationView 가 visible 되도록 해보자

 

3. GNB 클릭 이벤트 설정

MainActivity 에서 햄버거버튼을 클릭했을때,

메뉴영역이 노출되어있으면 visibility = gone, 미노출되어있으면 visible 상태로 만들어보자.

 

하지만 햄버거버튼, 뒤로가기, 타이틀 로고 이미지클릭시 실행되는 동작들이 Activity 별로 달라야할때가 있다.

즉 include 된 페이지의 이벤트를 부모 페이지에게 알려줘야할때 쓰는 방법을 사용해보겠다

 

1) TitleBar 상수 추가

Title bar 에서 어떤 동작을 수행했는지 알려주기 위해서 상수를 추가했다

class TitleBar : FrameLayout {
    companion object {
        const val BUTTON_NAME_BACK = 1 //뒤로가기
        const val BUTTON_NAME_HOME_MENU = 2 //햄버거버튼
        const val BUTTON_NAME_HOME = 3 //타이틀
    }
    ...
}

 

뒤로가기, 햄버거버튼, 타이틀을 클릭했을때 각 상수값을 부모 페이지로 보내도록 하겠다

class TitleBar : FrameLayout {
    object {
        const val BUTTON_NAME_BACK = 1
        const val BUTTON_NAME_HOME_MENU = 2
        const val BUTTON_NAME_HOME = 3
        const val BUTTON_NAME_CLOSE = 4
    }
...
    private fun initView() {

        //햄버거버튼 클릭 이벤트
        binding?.ivMenu?.setOnClickListener() {
            ///이곳에서 상수값을 보내고 싶음
        }
    }
...
}

 

자 어떻게 보내야할까?

 

내가 택한 방법으로는 DataChangeListener를 이용하는것이다.

 

리스너를 하나 만들고 부모에서 해당리스너를 오버라이딩 시켜서 사용하는 방법이다. 차근차근 해보자

 

2) DataChangeListener 추가

TitleBar 하단에 interface로 리스너를 하나 만들어준다.

그리고 그 안에 메소드를 하나 추가해서 파라미터로 상수값을 제공받는다

class TitleBar : FrameLayout {
    object {
        const val BUTTON_NAME_BACK = 1
        const val BUTTON_NAME_HOME_MENU = 2
        const val BUTTON_NAME_HOME = 3
        const val BUTTON_NAME_CLOSE = 4
    }
...

    private var binding: IncTitlebarBinding? = null
    private var dataChangeListener: DataChangeListener? = null
    
    private fun initView() {

        //햄버거버튼 클릭 이벤트
        binding?.ivMenu?.setOnClickListener() {
            if(dataChangeListener != null)
                this.dataChangeListener?.onChanged(BUTTON_NAME_HOME_MENU,"")
        }
    }
...

    interface DataChangeListener {
        fun onChanged(index: Int,  value : String);
    }

}

 

햄버거버튼을 클릭했을 때, 해당 리스너의 onChanged 메소드로 상수값을 전달한다

그렇다면 이것을 부모가 쓰게끔 컨트롤가능한 메소드를 하나 추가해준다

 

3) 부모페이지 전용 메소드 추가

class TitleBar : FrameLayout {
    object {
        const val BUTTON_NAME_BACK = 1
        const val BUTTON_NAME_HOME_MENU = 2
        const val BUTTON_NAME_HOME = 3
        const val BUTTON_NAME_CLOSE = 4
    }
...

    private var binding: IncTitlebarBinding? = null
    private var dataChangeListener: DataChangeListener? = null
    
    private fun initView() {

        //햄버거버튼 클릭 이벤트
        binding?.ivMenu?.setOnClickListener() {
            if(dataChangeListener != null)
                this.dataChangeListener?.onChanged(BUTTON_NAME_HOME_MENU,"")
        }
    }
...

	//부모 사용 전용 메소드
    fun setOnClickBackBtnListener(listener: (Int, String) -> Unit) {
        this.dataChangeListener = object : DataChangeListener {
            override fun onChanged(index: Int, value: String) {
                listener(index, value)
            }
        }
    }

    interface DataChangeListener {
        fun onChanged(index: Int,  value : String);
    }

}

 

이렇게 메소드를 만들어서 뚫어줘야 부모에서 사욯할 수 있다.

 

전체 코드는 이렇다

class TitleBar : FrameLayout {
    companion object {
        const val BUTTON_NAME_BACK = 1
        const val BUTTON_NAME_HOME_RIGHT_SIDE = 2
        const val BUTTON_NAME_HOME = 3
        const val BUTTON_NAME_CLOSE = 4
    }

    constructor(context: Context) : super(context) {
        inflateView()
        initView()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        inflateView()
        initView()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        inflateView()
        initView()
    }

    private var binding: IncTitlebarBinding? = null
    private  var dataChangeListener: DataChangeListener? = null

    private fun inflateView() {
        binding = IncTitlebarBinding.inflate(LayoutInflater.from(context))
        addView(binding?.root)
    }

    private fun initView() {

        //클릭이벤트
        binding?.ivMenu?.setOnClickListener() {
            if(dataChangeListener != null)
                this.dataChangeListener?.onChanged(BUTTON_NAME_HOME_RIGHT_SIDE,"")
        }
    }

    /**
     * set title (TEXT)
     */
    fun setTitle(title:String) {
        if (title.isNullOrEmpty()) {
            binding?.tvTitle?.hide()
        } else {
            binding?.tvTitle?.show()
            binding?.tvTitle?.text = title
        }
    }

    fun showMenuIcon(b: Boolean) {
        if (b) {
            binding?.ivMenu?.show()
        } else {
            binding?.ivMenu?.hide()
        }
    }

    fun setOnClickBackBtnListener(listener: (Int, String) -> Unit) {
        this.dataChangeListener = object : DataChangeListener {
            override fun onChanged(index: Int, value: String) {
                listener(index, value)
            }
        }
    }

    interface DataChangeListener {
        fun onChanged(index: Int,  value : String);
    }

}

 

 

4.  Main Activity 에서 리스너 사용

setTitleBar 부분에서 아래와같이 해당 메소드를 이용할 수 있다

class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null
    lateinit var drawerLayout: DrawerLayout

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

    private fun setTitleBar () {
        binding?.incTitlebar?.setTitle("MAIN")
        binding?.incTitlebar?.showMenuIcon(true)
        binding?.incTitlebar?.setOnClickBackBtnListener { i, s ->
            when (i) {
                TitleBar.BUTTON_NAME_HOME -> {} //로고 클릭 event
                TitleBar.BUTTON_NAME_BACK -> {} //뒤로가기 event
                TitleBar.BUTTON_NAME_HOME_MENU -> { //햄버거버튼 event
                    if (binding?.navView?.isVisible == true) {
                        binding?.navView?.hide()
                    } else {
                        binding?.navView?.show()
                    }

                }
            }
        }
    }

}

 

뭐 모든 페이지가 같은 동작을 수행한다면 굳이 이럴필요는 없지만 

아닌경우가 많기때문에.. 해당방법을 사용했다.

 

중심적으로 봐야할점은 부모로 이벤트를 보내는 방법!! 나중에 fragment 에서 리스트클릭하거나 다양한 이벤트를 보낼때 이러한 방식을 사용하게된다.

 

완성본!

 

 

 

 

 

728x90
반응형

1. 타이틀 UI 생성

 

보편적인 안드로이드 타이틀 모양을 제작해보자

위치는 layout 폴더 안에 inc_titlebar 라는 이름의 파일을 만들것이다

inc 의 의미는 include 를 할 파일이라는 의미인데, 액티비티에 타이틀 연결할때 예제를 봐보자

 

타이틀 디자인은 왼쪽에 햄버거 버튼이 있고, 가운데에 타이틀 텍스트가 오도록 한다

 

1) 타이틀 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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/layout_title"
            android:layout_width="match_parent"
            android:layout_height="@dimen/title_height"
            android:layout_marginLeft="12dp"
            android:layout_marginRight="12dp">

            <!-- back button -->
            <ImageView
                android:id="@+id/iv_back"
                style="@style/AppTheme.Button.SmallIcon.titleBar"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical|left"
                android:visibility="gone"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/btn_back" />

            <ImageView
                android:id="@+id/iv_menu"
                style="@style/AppTheme.Button.SmallIcon.menu"
                android:layout_width="@dimen/title_height"
                android:layout_height="match_parent"
                android:layout_gravity="center_vertical"
                android:visibility="gone"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/ic_menu" />

            <!-- title -->
            <TextView
                android:id="@+id/tv_title"
                style="@style/AppTheme.Text.Title"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="60dp"
                android:layout_marginRight="60dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="title" />

            <!-- border line -->
            <View
                android:id="@+id/vw_line"
                android:layout_width="match_parent"
                android:layout_height="8dp"
                android:layout_gravity="bottom"
                android:background="@drawable/gradient_stroke_gray"
                app:layout_constraintBottom_toBottomOf="parent" />

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

</layout>

 

햄버거버튼부분은, 뒤로가기 이미지가 올때도 있어야 하므로 ImageView 두개를 넣어뒀다.

@Style, @drawable, @dimen 부분은 사용성을 좋게하기위해 했지만 안해도되고, 직접 속성을 설정해줘도 되므로 생략!

 

자세한 코드는 아래 깃주소 참고

https://github.com/Daseul727/mobile_sample.git

 

2) 타이틀 소스코드 작성

TitleBar.kt 파일을 만들어주자

 

아래 소스코드 중 FrameLayout 을 import 하면 기본 constructor 세개를 넣어야지 오류가 발생하지않는다.

class TitleBar : FrameLayout {

    constructor(context: Context) : super(context) {
        inflateView()
        initView()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        inflateView()
        initView()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        inflateView()
        initView()
    }

    private var binding: IncTitlebarBinding? = null

    private fun inflateView() {
        binding = IncTitlebarBinding.inflate(LayoutInflater.from(context))
        addView(binding?.root)
    }

    private fun initView() {

        //클릭이벤트

    }

    /**
     * set title (TEXT)
     */
    fun setTitle(title:String) {
        if (title.isNullOrEmpty()) {
            binding?.tvTitle?.hide()
        } else {
            binding?.tvTitle?.show()
            binding?.tvTitle?.text = title
        }
    }

    fun showMenuIcon(b: Boolean) {
        if (b) {
            binding?.ivMenu?.show()
        } else {
            binding?.ivMenu?.hide()
        }
    }
}

 

initView 쪽에는 햄버거버튼 클릭이벤트를 달아주면 좋을것같고

나머지 하단의 function 들은, 외부에서 햄버거버튼을 보여줄지? 타이틀을 숨질지 동작을 위한 메소드이다.

 

3) Activity 에 타이틀 추가

 

MainActivity 에 타이틀을 추가해보겠다.

<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

////추가////
    <com.example.basic_mobile.ui.TitleBar
        android:id="@+id/inc_titlebar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/title_height"
        app:layout_constraintTop_toTopOf="parent"/>
////////

    <Button
        android:id="@+id/btn_recycler"
        android:text="기본 Recycler View"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        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_epoxy"
        android:text="epoxy View"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_recycler"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

위와같은 방법으로 TitleBar 를 include 한다

 

그리고 Activity 에서 햄버거버튼 및 title 의 text 를 설정한다

 

class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

    private fun setTitleBar () {
        binding?.incTitlebar?.setTitle("MAIN")
        binding?.incTitlebar?.showMenuIcon(true)
    }
}

 

이렇게하면 타이틀 추가 완료

 

728x90

+ Recent posts