반응형

 

Health Connect API 가이드 문서가 있지만.. java 기준이기도하고, 실제로 적용해보니 이슈가 몇가지생겨서 적용하는방법을 정리해보고자 한다.

 

가이드문서 URL : https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started?hl=ko

 

헬스 커넥트 시작하기  |  Android health & fitness  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 헬스 커넥트 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에서는 앱에서 헬스

developer.android.com

 

시작하기에 앞서 안드로이드 버전을 확인하면 좋은데, 나는 안드로이드 13버전으로 테스트 해보았고

이 경우 health connect 앱이 설치되어있지 않았고 설치 후에도 삼성헬스데이터가 넘어오지 않았다.

 

헬스커넥트 메인페이지 최근 정보에 엑세스한 앱에도 뜨지않았고 데이터 및 액세스 탭에도 데이터가 아예 없었다.

 

이 경우 삼성헬스 앱 - 상단 점세개 -> 문의하기 에서 헬스커넥트로 데이터연동 안되어있다고 문의하면 하루..정도 걸려서 해주신다..ㅎㅎ..참고..

 

1. 라이브러리 임포트 및 권한 추가

 

우선 build.gradle 에 health connect client 를 import 해준다

    /*Health Connect*/
    implementation "androidx.health.connect:connect-client:1.1.0-alpha02"

 

그리고 manifest 에 내가 취득하고싶은 권한을 넣는다.

권한종류에 관해선 가이드 문서에 상세히 나와있으니 참고하도록 하자.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   ...
   
       <!--Health Connect Permissions-->
    <uses-permission android:name="android.permission.health.READ_STEPS"/>
    <uses-permission android:name="android.permission.health.READ_SLEEP"/>
    <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
    <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
    <uses-permission android:name="android.permission.health.READ_DISTANCE"/>
    <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
    <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED"/>

    <!--Health Connect-->
    <queries>
        <package android:name="com.google.android.apps.healthdata" />
    </queries>

    ...
    
</manifest>

 

내가 원하는 권한 종류들을 추가하였다

 

2. Health Connect API 호출

 

해당 API 를 호출하기 원하는 activity 혹은 fragment에서 호출하면 된다.

 

class MainFragment : Fragment() {

    private var TAG : String = "MAIN FRAGMENT"
    private var _binding : FragmentMainBinding? = null
    private val binding : FragmentMainBinding
        get() = _binding!!

    private lateinit var healthConnectClient: HealthConnectClient
    
    //manifest 에서 선언한 권한과 동일해야함
    private val permissionList = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getReadPermission(SleepSessionRecord::class),
        HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
        HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
        HealthPermission.getReadPermission(DistanceRecord::class),
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getReadPermission(FloorsClimbedRecord::class)
    )

    private val requestPermissions = registerForActivityResult(
        PermissionController.createRequestPermissionResultContract()
    ) { granted ->
        if (granted.containsAll(permissionList)) {
            Log.d("MAIN ACTIVITY", "requestPermissions success ==== ")
            CoroutineScope(Dispatchers.IO).launch {
                readStepsData()
            }
        } else {
            CustomToast.createToast(requireContext(), "건강정보 가져오기에 실패하였습니다.\n권한을 추가하거나 헬스 커넥트 앱을 다운로드 해주세요.")?.show()
            Log.d("MAIN ACTIVITY", "requestPermissions fail ////////////////// ")
            activity?.finish()
            openPlayStoreForHealthConnect()
        }
    }

    companion object {
        fun newInstance(): MainFragment {
            val fragment = MainFragment()
            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_main, container, false)
        return binding.root
    }

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

        // Health Connect
        CoroutineScope(Dispatchers.Main).launch {
            connectHealthData()
        }

    }

    private fun connectHealthData() {
        // 1. Health Connect 앱 유무 확인
        val availabilityStatus = HealthConnectClient.getSdkStatus(requireContext(), Constants.HEALTH_CONNECT_PACKAGE_NAME)
        if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
            CustomToast.createToast(requireContext(), "헬스 커넥트 앱을 다운로드 해주세요")?.show()
            activity?.finish()
            openPlayStoreForHealthConnect()
        }

        // 2. Health connect sdk update 확인
        if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
            CustomToast.createToast(requireContext(), "헬스 커넥트 업데이트가 필요합니다")?.show()
            activity?.finish()
            openPlayStoreForHealthConnect()
        }

        // 4. Check permissions and run
        healthConnectClient = HealthConnectClient.getOrCreate(requireContext())
        requestPermissions.launch(permissionList)
    }


    private suspend fun readStepsData() {
        val now: LocalDateTime = LocalDateTime.now()
        val startOfDay = LocalDateTime.of(now.toLocalDate(), LocalTime.MIDNIGHT)

        val request = ReadRecordsRequest(
            recordType = StepsRecord::class,
            timeRangeFilter = TimeRangeFilter.between(startOfDay, now),
        )

        val response = healthConnectClient.readRecords(request)
        val steps = response.records.sumOf { it.count }

        //토탈 칼로리
        val totalCaloriesRequest = ReadRecordsRequest(
            recordType = TotalCaloriesBurnedRecord::class,
            timeRangeFilter = TimeRangeFilter.between(startOfDay, now),
        )
        val totalCaloriesResponse = healthConnectClient.readRecords(totalCaloriesRequest)
        val totalCalories = totalCaloriesResponse.records.sumOf { it.energy.inCalories }

        //활동 칼로리
        val activeCaloriesRequest = ReadRecordsRequest(
            recordType = ActiveCaloriesBurnedRecord::class,
            timeRangeFilter = TimeRangeFilter.between(startOfDay, now),
        )
        val activeCaloriesResponse = healthConnectClient.readRecords(activeCaloriesRequest)
        val activeCalories = activeCaloriesResponse.records.sumOf { it.energy.inCalories }

        CoroutineScope(Dispatchers.Main).launch {
            binding.tvStepCount.text = steps.toString()
            binding.tvTotalCaloriesCount.text = totalCalories.toString()
            binding.tvActiveCaloriesCount.text = activeCalories.toString()
        }

    }

    private fun openPlayStoreForHealthConnect() {
        val intent = Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata")
            setPackage("com.android.vending")
        }
        startActivity(intent)
    }
}

 

삼성헬스 데이터가 health connect 에 연결되었는가?

health connect 버전이 최신인가?

health connect 가 설치되어있는가?

 

등등 우선시 되어야 할 상황들을 체크하는 로직도 추가하였다.

 

3. Permission Activity 추가

 

이상하게 이걸 추가안하면..연결 계속 fail 뜬다.

아직 파악이 덜됨..

 

1) PermissionActivity.kt 추가

@AndroidEntryPoint
class PermissionActivity : AppCompatActivity(){
    private lateinit var binding: ActivityPermissionBinding

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

        val permissions = setOf(
            HealthPermissions.READ_STEPS
        )

        val permissionController = HealthConnectClient.getOrCreate(this).permissionController
        //permissionController.pe(permissions, this)

    }

    private val permissionRequest = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) {
        // 권한 요청 후의 결과 처리
        // 필요한 경우 MainActivity로 돌아가게 함
        startActivity(Intent(this, MainActivity::class.java))
    }

 

 

2) layout 추가

<?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"
        tools:context=".PermissionActivity">

        <TextView
            android:text="test - permission"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/nav_area"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/navigation_menu"
            app:itemIconTint="@color/black"
            app:labelVisibilityMode="labeled"
            app:itemTextColor="@android:color/transparent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

3) Manifest 수정

  ....
  
  <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".ui.permission.PermissionActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
            </intent-filter>
        </activity>
        ...

 

해당코드는 깃헙에 올려두었다.

 

그런데, PermissionActivity 추가하는 로직에 관해선, 공식문서에 나와잇지만 안드로이드12이상부터는 아래와같이써야한다

 <activity
            android:name=".ui.permission.PermissionActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
            </intent-filter>
        </activity>

        <activity-alias
            android:name="ViewPermissionUsageActivity"
            android:exported="true"
            android:targetActivity=".ui.permission.PermissionActivity"
            android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
            <intent-filter>
                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
                <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
            </intent-filter>
        </activity-alias>

 

아래 링크의 Fitness 프로젝트!

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

 

+

2024.10.29

하지만 결국..데이터 잘 안뽑혀서 삼성헬스 -> 점세개 -> 설정 -> 고객센터 -> 1:1문의에서 헬스커넥터로 데이터 안들어온다고 캡쳐화면이랑 문의글 올리면...들어오게 처리해준다 ㅠ 답변은 따로없다..다음날이나 몇시간 후 확인해보면됨..

728x90
반응형

 

Kotlin Native Android App 개발 중 걸음수가 필요해서 Samsung Health API 를 달려고 했는데

결론적으로는 Samsung partnership 등록이 필요했고, 현재로서는 더이상 받고있지않는다는 답변을 받았다.

질문 올렸던 URL : https://forum.developer.samsung.com/t/samsung-health-api-is-not-working-for-android-device/33200/2

 

하지만 작업한게 아까워서 기록용으로 남기려고한다.

 

 

 

이 모든것은 작동안되는 방법이므로 health connect 앱으로 작업하세요!!

 

 

 

1. SDK 다운로드

 

SDK 추가방법은 build.gradle 에 추가하거나 직접 파일추가하는 방법이 있다.

build.gradle 에 추가했을때 import가 되지 않아서 나는 아래 링크의 예제 프로젝트에서 받았다

(simplehealth\app\src\main\libs\samsung-health-data-1.5.0.aar)

https://developer.samsung.com/health/android/sample/simple-health.html

 

Data | SimpleHeath | Samsung Developer

The world runs on you.

developer.samsung.com

 

그리고 내 프로젝트의 libs에 넣고 build.gradle에 file 위치를 알려줬다.

    /*Samsung sdk*/
    implementation files('libs/samsung-health-data-1.5.0.aar')

 

2. AndroidManifest 수정


<manifest>

...
    <uses-permission android:name="android.permission.INTERNET"/>

    <uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!--삼성 SDK-->
    <queries>
        <package android:name="com.sec.android.app.shealth" />
    </queries>

    <application

        ....
    >

        <!--삼성 SDK 권한-->
        <meta-data android:name="com.samsung.android.health.permission.read"
            android:value="com.samsung.health.step_count;com.samsung.shealth.step_daily_trend;com.samsung.health.exercise"/>

        <activity
            android:name=".ui.main.MainActivity"
            ...
        />

	</application>

</manifest>

 

마지막 삼성 SDK 권한쪽 보면 value에 원하는 데이터를 콤마의 형태로 여러번 불러올수있다.

나는 읽기만 필요하니까 read 만 호출한것.

 

참고 가이드 URL : https://developer.samsung.com/health/android/data/api-reference/com/samsung/android/sdk/healthdata/HealthPermissionManager.html

 

 

3. Samsnung Health API 연동

 

1) initDataStore 메소드에서 HealthDataStore 를 초기화시키고 실행

2) onConnected 에서 필요한 권한을 세팅하고 (이때 Manifest에서 설정한 권한과 동일해야함) 권한 request

3) 그러면 권한 입력하는 팝업이 호출됨 

 

4) 권한 선택 여부가 mPermisionListener 로 들어옴

...

import com.samsung.android.sdk.healthdata.HealthConnectionErrorResult
import com.samsung.android.sdk.healthdata.HealthConstants
import com.samsung.android.sdk.healthdata.HealthConstants.Exercise
import com.samsung.android.sdk.healthdata.HealthConstants.StepDailyTrend
import com.samsung.android.sdk.healthdata.HealthDataResolver
import com.samsung.android.sdk.healthdata.HealthDataStore
import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener
import com.samsung.android.sdk.healthdata.HealthPermissionManager
import com.samsung.android.sdk.healthdata.HealthPermissionManager.PermissionKey
import com.samsung.android.sdk.healthdata.HealthPermissionManager.PermissionResult
import com.samsung.android.sdk.healthdata.HealthPermissionManager.PermissionType
import com.samsung.android.sdk.healthdata.HealthResultHolder


@AndroidEntryPoint
class HomeFragment : Fragment() {

    private var TAG : String = "HOME FRAGMENT"
    private lateinit var mStore: HealthDataStore
    private lateinit var mKeySet: MutableSet<PermissionKey>

	...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        //삼성 SDK 초기화
        initDataStore()
    }


    /**
     * 삼성 SDK 초기화
     */
    private fun initDataStore() {
    	//activity 에서 실행한다면 requireContext 대신 this
        mStore = HealthDataStore(requireContext(), mCntListener) 
        mStore.connectService()
    }

    /**
     * 삼성 SDK Listener 설정
     */
    private val mCntListener: ConnectionListener = object : ConnectionListener {
        override fun onConnected() {
            Log.d(TAG, "Health data service is connected.")

            val mManager = HealthPermissionManager(mStore)

            //내가 원하는 권한요청
            mKeySet = HashSet()
            mKeySet.add(PermissionKey(HealthConstants.StepCount.HEALTH_DATA_TYPE, PermissionType.READ))
            mKeySet.add(PermissionKey(StepDailyTrend.HEALTH_DATA_TYPE, PermissionType.READ))

            val resultMap : MutableMap<PermissionKey, Boolean> = mManager.isPermissionAcquired(mKeySet)

            if(resultMap.containsValue(false)) { //만약 권한이 없다면 권한요청 UI 팝업
                mManager.requestPermissions(mKeySet, requireActivity()).setResultListener(mPermissionListener)
            } else {
                binding.tvStepCountText.text = getStepCount().toString()
            }
        }

        override fun onConnectionFailed(error: HealthConnectionErrorResult) {
			//연결 실패 시 실행할 로직
        }

        override fun onDisconnected() {
            mStore.disconnectService() // 연결종료
        }
    }

    private val mPermissionListener = HealthResultHolder.ResultListener<PermissionResult> { result ->
        Log.d(TAG, "Permission callback is received.")
        val resultMap = result.resultMap

        if (resultMap.containsValue(false)) {
            Log.d(TAG, "Permission false")
        } else {
            binding.tvStepCountText.text = getStepCount().toString()
        }
    }
    
    private fun getStepCount() : Int{
    var stepCount = 0;

    val resolver = HealthDataResolver(mStore, null)
    val request = HealthDataResolver.ReadRequest.Builder()
        .setDataType(HwConstants.DATA_TYPE_STEP_COUNT)
        .build()

    resolver.read(request).setResultListener { result ->
        val iterator = result.iterator()
        while (iterator.hasNext()) {
            val data = iterator.next()
            stepCount = data.getInt("count")
        	}
        	result.close()
    	}
    	return stepCount
	}
}

 

 

나의 경우 3번을 진행하다가 아무리해도 권한체크하는 영역이 노출되지않아서 문의를 했었다.

 

모든권한 하단에 내가 요청한 권한목록이 떴어야 했는데... 

 

결론적으로는 해당 API 는 삼성 파트너쉽 앱 등록 후 사용하여야하는데 현재 서비스 리뉴얼중으로 (몇달간 계속되는듯..) 사용불가.

 

걸음수 가져가고싶으면 헬스 커넥트 앱을 사용하라는 답변!

후... 이거 다 지우고 헬스 커넥트 앱으로 재도전 ^^....

 

728x90
반응형

안드로이드 스튜디오 사용중에 update 가 필요해서 진행하고 다시 켰더니

 

the environment variable java_home does not point to a valid jvm installation androidstudio

 

이유를 찾아보면 JAVA_HOME URL 변경하라고 나오는데 나는 그 케이스도 아니었다.

 

update 말고 그냥 삭제 후 재설치 하니까 잘됨;;

update할때 뭔가 꼬인듯

728x90
반응형

 

TextView 등 기타 요소를 사용하여 키보드를 활성화시키면 키보드의 화살표를 눌러야 키보드가 숨겨진다

 

하지만 보통 사용자가 사용할대는 화면을 클릭했을때 없어지길 바라는데,

Activity 에 아래의 로직을 선언하면 키보드는 감춰진다

 

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    val imm: InputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(currentFocus?.windowToken, 0)

    return super.dispatchTouchEvent(ev)
}

 

728x90
반응형

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

+ Recent posts