반응형

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

 

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

 

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

+ Recent posts