반응형

저번 포스팅에서는 프로젝트 세팅을 했는데 일단 거지같은 모양새다.

 

밑에 푸터도 주고 위에 헤더도 설정해보자

 

1. layout 나누기

header / body / footer 영역을 나눠보자.

우선 home : Scaffold 설정을 하고 appBar / body / bottomNavigationBar 로 나누면 된다

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {

    return MaterialApp(
        home: Scaffold(
            appBar: AppBar(
              backgroundColor: Colors.white,
              title: SizedBox(
                height: 40,
                child: TextField(
                  decoration: InputDecoration(
                      hintText: '검색어를 입력하세요', //힌트 텍스트
                      filled: false, //배경색 활성화
                      fillColor: Colors.white,
                      suffixIcon: const Icon(Icons.search, color: Colors.grey),
                      enabledBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(20),
                          borderSide: const BorderSide(color: Colors.grey, width: 1)
                      ),
                      focusedBorder: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(20),
                          borderSide: const BorderSide(color: Colors.grey, width: 1)
                      ),
                      contentPadding: const EdgeInsets.only(left: 15, right: 10)
                  ),
                ),
              ),
            ),
            body: Container(
              width: 150, height: 50, color: Colors.black,
              margin: const EdgeInsets.fromLTRB(10, 10, 10, 10),
            ),
            bottomNavigationBar: const BottomAppBar(
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      Icon(Icons.star),
                      Icon(Icons.confirmation_num_sharp),
                      Icon(Icons.co2_outlined)]
                )
            )
        )
    );

  }
}

 

 

대충이렇게하면 뭔가 많이본 모양이 나오긴 한다

input 필드를 생성하고 그 안에 hint 와 suffix icon 돋보기 이미지를 설정하였다

 

하지만 상단의 시간쪽이랑 로고도 넣으면 좋을것같다.

 

2. Header 꾸미기

appBar 에 leading 속성을 추가해서 로고 이미지를 넣어보자.

 

이미지를 넣으려면 pubspec.yaml 에 이미지 root 설정을 해야한다.

assets 설정을 추가하자

...

flutter:
  assets:       # <- 여기 추가
    - assets/    # <- 여기 추가
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  ....

 

그리고 인텔리제이기준 우측상단에 pub get, pub upgrade를 한번씩 눌러주자

 

그리고 재실행해주면 이제뭔가 앱같긴 하다.

 

상단에 회색영역도 지워보자

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  // 상태 바 배경 색상 변경 (흰색으로 설정)
  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
    statusBarColor: Colors.white, // 상태 바 색상을 흰색으로 설정
    statusBarIconBrightness: Brightness.dark, // 상태 바 아이콘을 어두운 색으로 설정 (흰색 배경에 어울리도록)
  ));

  runApp(const MyApp());
}

 

이렇게까지하면 헤더는 정리된 느낌 근데 폰트가 맘에안든다.

 

국룰폰트 Noto Sans Kr 을 넣어보자

 

3. 폰트 추가

https://fonts.google.com/noto/specimen/Noto+Sans+KR

 

Noto Sans Korean - Google Fonts

Noto is a global font collection for writing in all modern and ancient languages. Noto Sans KR is an unmodulated (“sans serif”) design for the Korean language u

fonts.google.com

 

여기서 get Font -> Download All

해서 모든 ttf 파일을 다운로드하고 프로젝트의 root 경로에 fonts 폴더생성 후 모두 집어넣는다

 

이걸이제 어케쓰냐면, pubspec 에 선언하고 사용하면 된다

pubspec 하단에 example이 나와있긴한데, 만드려면 복잡시러우니까 그냥 밑에 복사해서 쓰자

 

헷깔릴까봐 flutter 부터 복사해봄

flutter:
  assets:
    - assets/

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  fonts:
    - family: NotoSansKR
      fonts:
        - asset: fonts/NotoSansKR-Thin.ttf
          weight: 100
        - asset: fonts/NotoSansKR-ExtraLight.ttf
          weight: 200
        - asset: fonts/NotoSansKR-Light.ttf
          weight: 300
        - asset: fonts/NotoSansKR-Regular.ttf
          weight: 400
        - asset: fonts/NotoSansKR-Medium.ttf
          weight: 500
        - asset: fonts/NotoSansKR-SemiBold.ttf
          weight: 600
        - asset: fonts/NotoSansKR-Bold.ttf
          weight: 700
        - asset: fonts/NotoSansKR-ExtraBold.ttf
          weight: 800
        - asset: fonts/NotoSansKR-Black.ttf
          weight: 900

 

그리고 main.dart 에서 hoem 상단에 theme 을 추가하자

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {

    return MaterialApp(
        theme: ThemeData( //추가
            fontFamily: 'NotoSansKR'
        ),
        home: Scaffold(
        ...생략

 

 

4. Footer 추가

푸터를 추가한다는것은 클릭할때마다 페이지가 바뀌면 좋겠기 때문이다.

flutter 페이지 이동방식에는 두가지가 있다.

 

header, footer 를 유지하지않고 전체페이지가 바뀌는방식과 header,footer를 고정시키고 body 만 바뀌는 방식이 있는데, 이번엔 body 만 바꿔보겠다.

 

푸터에 탭을 3개둔다고 가정하고 이동할 페이지를 3개 더 생성해보자

 

main.dart는 페이지변경을 관장하는곳으로 인식하고 나머지페이지는 이동할 대상이다.

나는 달력을 넣을것이기때문에 home, calendar, mypage 이렇게 3개로 만들어봤다.

 

home.dart

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('홈 화면'),
      ),
    );
  }
}

 

calendar.dart

import 'package:flutter/cupertino.dart';

class CalendarScreen extends StatelessWidget {
  const CalendarScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('달력 화면'),
    );
  }
}

 

mypage.dart

import 'package:flutter/material.dart';

class MyPageScreen extends StatelessWidget {
  const MyPageScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('여기는 마이페이지 화면입니다!'),
      ),
    );
  }
}

 

이렇게 3개를 만들었으면 이제 main.dart에 이어보자

 

MyApp build 부분을 이렇게 간단하게 바꿔놓는다

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(fontFamily: 'NotoSansKR'),
      home: MainScreen(), // MainScreen이 앱의 첫 화면이 됨
    );
  }
}

 

그리고 MainScreen 을 정의해준다.

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(fontFamily: 'NotoSansKR'),
      home: MainScreen(), // MainScreen이 앱의 첫 화면이 됨
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  _MainScreenState createState() => _MainScreenState();
}

 

이제 MainScreen State 에 들어갈 요소들을 넣어주자

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(fontFamily: 'NotoSansKR'),
      home: MainScreen(), // MainScreen이 앱의 첫 화면이 됨
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _selectedIndex = 0;

  static final List<Widget> _widgetOptions = <Widget>[
    HomeScreen(), //import 필요 
    CalendarScreen(),  //import 필요
    MyPageScreen(),  //import 필요
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: Header(),
      body: _widgetOptions.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '홈',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.calendar_today),
            label: '달력',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '마이페이지',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.blue,
        onTap: _onItemTapped,
      ),
    );
  }
}

 

요로코롬하면 탭 클릭 시 페이지 변경 완료

728x90

'Mobile > Hybrid' 카테고리의 다른 글

Flutter 프로젝트 시작하기 (intelliJ)  (1) 2025.03.25
Cordova (코르도바) 시작하기  (1) 2024.09.06
반응형

 

하이브리드앱으로 코르도바를 쓰다가 html, css, js 범벅이 힘들어서 flutter를 공부해봤다.

 

기본적인 jdk, gradle 은 있다고 생각하겠다.

 

1. Flutter SDK 다운로드

https://docs.flutter.dev/release/archive

 

Flutter SDK archive

All current Flutter SDK releases: stable, beta, and main.

docs.flutter.dev

 

(Windows 기준)

이곳에서 stable SDK 버전을 다운받고 개인 PC 내 특정 폴더에 위치시킨다.

 

그리고 환경변수 PATH 에 해당 위치를 등록.

나의 경우는 D:\bin\flutter\bin 이었고 bin 까지만 하면된다

 

2. IntelliJ 플러그인 flutter 추가

File -> Setting -> Plugins -> Dart, Flutter 플러그인을 추가한다.

 

3. 프로젝트 생성

New Project -> Flutter 생성한다.

이때 Flutter SDK Path 에 내가 환경변수에 설정했던 flutter 위치가 와있으면 성공

 

 

패키지명 설정해서 대강 만들고 실행하면 된다.

 

나는 ios, android 만 만들거라서 이렇게했는데

빌드할때마다 모바일로 보지말고 web을 선택해서 browser도 확인하는것도 좋을것같다.

4. 개발시작

main.dart 가 첫 시작이다.

보통 lib > main.dart  얘를 index로 보는거같고 각종 header, footer 와 같은 layout 성 화면들을 [widgets]폴더에,

기타화면은 [screens] 폴더에 넣는것같다

 

하여간 처음 실행시키면 main.dart 에 알수없는 코드가 많은데 싹다지우자

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {

    return const MaterialApp(
        home: Text("test")
    );

  }
}

 

이렇게 설정해주면 아래와 같이 보일것이다.

 

나는 갤럭시 23을 테스트모델로 작업했고, release 버전이 아닌 이상, 보이는바와 같이 우측상단에 DEBUG 라고 보인다.

 

다음포스팅에선 페이지이동과 layout 설정을 해보겠다

728x90

'Mobile > Hybrid' 카테고리의 다른 글

Flutter 프로젝트 시작하기 - 페이지 이동  (0) 2025.03.25
Cordova (코르도바) 시작하기  (1) 2024.09.06
반응형

작업은 vscode 에서 진행한다.

만약 없다면 무료 툴 이므로 다운로드!

 

1. node js 설치

https://nodejs.org/en

 

 

2.  cordova 설치

cmd 창에서 진행하면 된다

$ npm install -g cordova

 

3. cordova 프로젝트 생성

내가원하는 폴더로 이동 후 아래 커맨드 실행

$ cordova create [프로젝트명] [패키지명] [프로젝트명]

ex)
$ cordova create cordova1 com.example.test cordova1

 

이렇게 프로젝트가 생성된다

 

처음에는 기본적으로 www 즉 웹만 생성되며 실행시켜볼수도있다.

 

vscode 플러그인으로 live server 를 추가하고 (구글링) 실행하면 코르도바 이미지가 보인다

 

 

 

4.  Android, iOS platform 추가

아래 커맨드로 안드로이드 소스코드를 추가할 수 있다

$ cordova platform add android

 

마찬가지로 ios로 추가해주면 아래와같이 platoforms에 android 와 ios가 생긴다

 

Android 같은 경우는, java 기반으로 소스코드가 생기는것을 확인해 볼 수 있다.

 

5. project build

 

이제 이것을 vscode에서 그대로 build 하려면 ios의 경우는 맥북이 필요하고

android 는 build 가능하나, 필요한 requirements들이 있다 (jdk 등등) 대신 안드로이드 스튜디오에서 해당프로젝트 실행해도됨

 

$ cordova requirements

 

나의 경우 위에서부터 하나씩 보면 

jdk : 17버전 있음

sdk  : 있음

android target : 없음

gradle : 없음

macOS : 없음

 

이렇게 확인된다

 

특히 android target 부분이나 밑에 gradle 을 보면 please install android studio 라는 부분을 볼수있는데 안드로이드 스튜디오에서 해당 프로젝트를 빌드하는게 속편할것같다

728x90

'Mobile > Hybrid' 카테고리의 다른 글

Flutter 프로젝트 시작하기 - 페이지 이동  (0) 2025.03.25
Flutter 프로젝트 시작하기 (intelliJ)  (1) 2025.03.25
반응형

 

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

+ Recent posts