본문 바로가기
코딩일기/날씨앱 만들기 프로젝트

날씨 앱 만들기 : FusedLocationProvider 사용법 Part 2 (MVVM / Java)

by 욱파이어니어 2021. 9. 25.
728x90
반응형

이전에는 FusedLocationProvider 사용법의 6단계까지 알아봤다.

 

1. build.gradle(프로젝트) 파일에 classpath 추가하기.

2. build.gradle(Module:app) 파일에 dependencies 추가하기

3. manifest에 권한 추가하기.

4. Activity에서 위치정보 권한 확인하기.

5. 권한 확인 요청에 관한 콜백 메소드 만들기

6. 구글 플레이 서비스에서 위치정보 사용하는지 안하는지 체크하기 - 이전 포스트-

7. 구글 플레이 서비스 위치정보 사용 콜백 메소드 만들기

8. 위치 정보 업데이트 요청 메소드 만들기

9. 위치정보 업데이트 콜백 메소드 만들기

 

그럼 이제는 다음 단계인 7단계부터 알아보자.

 

https://wpioneer.tistory.com/193

 

날씨 앱 만들기 : FusedLocationProvider 사용법 Part 1 (MVVM / Java)

이전에 이미 위치정보를 LocationManager를 사용해서 위치정보를 받아왔었다. https://wpioneer.tistory.com/190 날씨 앱 만들기 : 안드로이드 GPS 정보 받아오기 날씨 앱을 만들기 위해선 필수적으로 필요한 GP

wpioneer.tistory.com

 

7. 구글 플레이 서비스 위치정보 사용 콜백 메소드 만들기

6번 과정에서 이제 구글플레이 서비스 사용에 대한 요청 메시지를 띄우게 했다.

그래서 우리의 선택에 따라 콜백되는 콜백 함수를 만들어야 한다.

 

소스는 아래와 같다.

    @Override
    public void onActivityResult(int requstCode,int resultCode,Intent data) {

        super.onActivityResult(requstCode, resultCode, data);
        if(requstCode == 200) { //요청코드가 200과 같다면
            if (resultCode == RESULT_OK) { //만약 구글플레이 서비스를 사용한다고하면
                checkLocationSetting();
            } else {
                Toast.makeText(getApplicationContext(), "위치정보를 승인하지 않으면 현재위치 기반으로 \n날씨정보를 알려드릴수 없습니다.", Toast.LENGTH_LONG).show();
                Log.i(TAG, "위치정보를 허가 안해줌");

                //허가를 받지 못했을때의 결과를 받았을때 ViewModel의 메소드를 호출하면됨
                mavm.defaultLocation();// I call ViewModel at this part - This works
                obeserveAPI();
            }
        }
    }

 

나는 6번에서 요청을 할떄 requestCode를 200으로 줬기때문에 requestCode가 200일때 예 아니오를 확인하게 했다.

 

자세한 내용은 주석으로 설명해놨으니 확인해면 될것 같다.

 

 

 

8. 위치 정보 업데이트 요청 메소드 만들기

 

그럼 이제 위치정보 업데이트 요청을 해보자.

나는 위치정보 업데이트는 ViewModel에서 하게 했기 때문에 ViewModel에 있는 요청 메소드를 호출하였다.

(ViewModel의 객체 생성은 해당 Activity의 onCreate()에서 했다.)

        //MAgencyViewModel 객체 생성
        mavm = new ViewModelProvider(this).get(MAgencyViewModel.class);

저렇게 구글플레이 서비스를 이용할수 있다고 되어 있으면 ViewModel의 위치 정보 업데이트 요청 메소드를

호출하는것이다.

 

그럼 이제 ViewModel의 위치 정보 업데이트 메소드의 소스는 아래와 같다.

    @SuppressLint("MissingPermission")//위치권한 체크안해도 된다고 하는 부분 안하는 이유는 SplashActivity에서 이미 했기 때문이다.
    public void requestUpdate(LocationRequest locationRequest){

        Log.d(TAG,"LocationRequest have been request");
        mldGi = new MutableLiveData<GeoInfo>(); //LiveData 객체 생성
        requestLocationUpdate = true;
        LocationServices.getFusedLocationProviderClient(getApplication())
                .requestLocationUpdates(locationRequest,lcb,null); //위치정보 업데이트 요청
    }

위 소스에 대한 설명은 주석으로 해놨으니 확인하면 될것 같다.

 

그럼 이제 위치정보 업데이트 요청에 대한 콜백메소드에 대해서 알아보자.

 

9. 위치정보 업데이트 콜백 메소드 만들기

위치정보 콜백 메소드는 위치정보에 대한 값을 받아왔을때 호출되는 메소드이다.

 

근데 일단 콜백메소드를 만들기 전에 우리는 LocationCallBack 변수를 만들어야 한다.

private LocationCallback lcb;

 

그럼 이제 해당 변수에 대한 객체를 생성하고 콜백 메소드를 만들어야 하는데 나는 객체 생성과 콜백 메소드를 

ViewModel의 생성자 안에 넣었다. 생성자 안에 넣은 이유는 LocationCallBack 변수에 대한 객체를 생성해야하는데

그부분은 생성자에서 하는게 제일 낫겠다고 생각했기도 했고 StackOverFlow에서 해당 방법을 추천해줬기 때문이다.

 

https://stackoverflow.com/questions/69296991/callback-method-onlocationchanged-is-not-working-on-viewmodel/69297614?noredirect=1#comment122522854_69297614 

 

Callback method onLocationChanged() is not working on viewmodel

I'm trying to get location information in ViewModel. So I called requestLocationUpdates() by getting LocationManager at View. I'm not sure requestLocationUpdates() works in viewmodel because callback

stackoverflow.com

 

소스는 아래와 같다.

    public MAgencyViewModel(@NonNull Application application) {
        super(application);
        Log.d(TAG,"LocationCallBack instance have been made");
        //LocationCallBack 부분 객체 생성하고 그에 LocatinResult 받았을때를 추상화 시켜주는 부분
        gpt = new GpsTransfer();
        lcb = new LocationCallback(){
            @Override
            public void onLocationResult(LocationResult locationResult){
                if(locationResult == null){
                    Log.d(TAG,"Location information have not been recieved");
                    return;
                }
                Log.d(TAG,"Location information have been recieved");
                //gps를 통하여서 위도와 경도를 입력받는다.
                for (Location location : locationResult.getLocations()) {
                    if (location != null) {
                        gpt.setLon(location.getLongitude()); //경도를 입력 받는다.
                        gpt.setLat(location.getLatitude()); //위도를 입력 받는다.
                    }
                }

                //gps 연결을 닫는다.
                LocationServices.getFusedLocationProviderClient(getApplication()).removeLocationUpdates(lcb);

                //x,y 좌표로 변환
                gpt.transfer(gpt,0);
                Log.d(TAG, gpt.toString());
                setGeoInfo(gpt); //변환된 정보를 GeoInfo에 넣음
                mldGi.setValue(gi); //LiveData에 데이터를 입력한다.
            }
        };
    }

ViewModel에서 getApplication()이 가능했던 이유는 ViewModel 클래스에서 AndroidViewModel

extends 했기 때문이다.

 

ViewModel에서 getApplication() 을 못해 고생했지만 일반적인 extends ViewModel 과는 다르게 Application이 있는

AndroidViewModel을 extends해서 쉽게 LocationUpdate를 요청할수 있었다.

 

무튼 이렇게 콜백 메소드를 하게 되면 이제 위치정보를 받아오게 되면

private MutableLiveData<GeoInfo> mldGi;

LiveData인  mldgi 에 값을 넣어 해당 LiveData에 변경사항이 생긴다.

 

그럼이제 해당 LiveData를 observe하는 Activity의 onChanage() 가 호출이 된다.

 

 

    public void observeGps(){
        mavm.getGeo().observe(this, new Observer<GeoInfo>() {
            @Override
            public void onChanged(GeoInfo geoInfo) {
                Log.i(TAG,mavm.getGeo().getValue().toString());
                mavm.callApi(mavm.getGeo().getValue());
                obeserveAPI();
            }
        });
    }

 

 

그럼 이제 전체 소스를 보여주겠다.

 

SplashActivity class

 

package wook.co.weather.view.splash;

import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;

import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResponse;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.android.gms.location.SettingsClient;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;

import wook.co.weather.R;
import wook.co.weather.models.dto.GeoInfo;
import wook.co.weather.models.dto.GpsTransfer;
import wook.co.weather.models.dto.ShortWeather;
import wook.co.weather.models.repository.MAgencyRepo;
import wook.co.weather.view.MainActivity;
import wook.co.weather.viewmodels.MAgencyViewModel;

public class SplashActivity extends AppCompatActivity{

    private ShortWeather sw;
    private MAgencyViewModel mavm;
    private final String TAG = "SplashActivity";

    private final int DEFAULT_LOCATION_REQUEST_PRIORITY = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; //배터리와 정확도를 밸런스있게 맞춰주는애
    private final long DEFAULT_LOCATION_REQUEST_INTERVAL = 2000L;
    private final long DEFAULT_LOCATION_REQUEST_FAST_INTERVAL = 10000L;

    private LocationRequest lr; //위치정보를 사용하기 위해서 사용하는 변수
    private FusedLocationProviderClient flpc;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash_screen);
        //MAgencyViewModel 객체 생성
        mavm = new ViewModelProvider(this).get(MAgencyViewModel.class);
        //LocationRequest 객체 생성
        lr = LocationRequest.create();
        checkLocationPermission();
    }

    public void checkLocationPermission(){
        //위치정보 권한 허용되어 있는지 아닌지를 확인하는 부분
        if(ActivityCompat.checkSelfPermission(SplashActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            //위치정보 권한 허용되어 있지 않다면 실행하는 코드, 여기서 request에 대한 응답이 나오면 아래에 있는 onRequestPermissionsResult() 함수를 콜백하게 됨
            ActivityCompat.requestPermissions(SplashActivity.this,new String[]{ Manifest.permission.ACCESS_FINE_LOCATION},200); //위치정보 권한을 요청한다.
        }else {
            //위치정보 권한이 허용되어 있을때 실행하는 코드
            Log.d(TAG, "위치정보 허용됨");
            //구글 플레이 서비스 위치정보 권한 승인된건지 확인해야함
            checkLocationSetting();
        }
    }

    //권한요청되었을때 콜백되어지는 함수
    @Override
    public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if(requestCode == 200){ //permissionCode가 200이고
            if(grantResults[0] == 0){ // 그중 가장 첫번째 result가 0 즉 승인된경우 진입
                Toast.makeText(getApplicationContext(),"위치정보 권한 승인됨",Toast.LENGTH_SHORT).show(); //위치정보 승인됐다고 알리고
                //위치정보 권한을 받았다면 진입
                if(ActivityCompat.checkSelfPermission(SplashActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED){

                    //구글 플레이 서비스 위치정보 권한 승인된건지 확인해야함
                    checkLocationSetting();
                }
            }else{ //위치정보를 허가받지 못했을경우 진입
                Toast.makeText(getApplicationContext(),"위치정보를 승인하지 않으면 현재위치 기반으로 \n날씨정보를 알려드릴수 없습니다.",Toast.LENGTH_LONG).show();
                Log.i(TAG,"위치정보를 허가 안해줌");

                //허가를 받지 못했을때의 결과를 받았을때 ViewModel의 메소드를 호출하면됨
                mavm.defaultLocation();// I call ViewModel at this part - This works
                obeserveAPI();
            }
        }
    }

    public void checkLocationSetting() {
        Log.d(TAG,"LocationReqeust is in setting");
        lr.setPriority(DEFAULT_LOCATION_REQUEST_PRIORITY);
        lr.setInterval(DEFAULT_LOCATION_REQUEST_INTERVAL);
        lr.setFastestInterval(DEFAULT_LOCATION_REQUEST_FAST_INTERVAL);

        SettingsClient settingsClient = LocationServices.getSettingsClient(this);
        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder().addLocationRequest(lr).setAlwaysShow(true);
        settingsClient.checkLocationSettings(builder.build())
                .addOnSuccessListener(this, new OnSuccessListener<LocationSettingsResponse>() { //구글 플레이 위치서비스를 사용할수 있을떄 진입
                    @Override
                    public void onSuccess(LocationSettingsResponse locationSettingsResponse) {
                        //구글플레이 위치 서비스 사용할수 있게 됐을때 진입
                        mavm.requestUpdate(lr);
                        observeGps();
                    }
                })
                .addOnFailureListener(SplashActivity.this, new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {//구글 플레이 위치서비스를 사용할수 없을떄 진입
                        int statusCode = ((ApiException) e).getStatusCode();
                        switch (statusCode){
                            case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: //충분히 설정 변경하는것으로 변경이 가능할떄
                                ResolvableApiException rae = (ResolvableApiException) e;
                                try {
                                    rae.startResolutionForResult(SplashActivity.this, 200);
                                } catch (IntentSender.SendIntentException ex) {
                                    Log.w(TAG,"LocationService approval canceled");
                                }
                                break;
                            case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: //GPS가 아예 없거나 연결자체가 불가능하여 사용을 물리적으로 사용하지 못할때
                                Log.w(TAG,"No way to change setting");
                                Toast.makeText(getApplicationContext(),"GPS 사용을 하지 못하여 위치정보를 받아오지 못하고 있습니다.\n GPS 연결을 해주세요.",
                                        Toast.LENGTH_SHORT).show();
                                mavm.defaultLocation();
                                obeserveAPI();
                                break;
                        }
                    }
                });
    }

    @Override
    public void onActivityResult(int requstCode,int resultCode,Intent data) {

        super.onActivityResult(requstCode, resultCode, data);
        if(requstCode == 200) {
            if (resultCode == RESULT_OK) {
                checkLocationSetting();
            } else {
                Toast.makeText(getApplicationContext(), "위치정보를 승인하지 않으면 현재위치 기반으로 \n날씨정보를 알려드릴수 없습니다.", Toast.LENGTH_LONG).show();
                Log.i(TAG, "위치정보를 허가 안해줌");

                //허가를 받지 못했을때의 결과를 받았을때 ViewModel의 메소드를 호출하면됨
                mavm.defaultLocation();// I call ViewModel at this part - This works
                obeserveAPI();
            }
        }
    }

    public void obeserveAPI(){
        mavm.getWeather().observe(this, new Observer<ShortWeather>() {
            @Override
            public void onChanged(ShortWeather shortWeather) {
                sw = mavm.getWeather().getValue();
                Log.i(TAG,sw.toString());

                Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //intent 형성한다.
                        Intent intent = new Intent(getApplicationContext(), MainActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);//액티비티 스택제거
                        //해당 intent에 객체를 실어서 보낸다.
                        intent.putExtra("shortWeather",sw);
                        startActivity(intent);
                    }
                },1000);
            }
        });
    }
    public void observeGps(){
        mavm.getGeo().observe(this, new Observer<GeoInfo>() {
            @Override
            public void onChanged(GeoInfo geoInfo) {
                Log.i(TAG,mavm.getGeo().getValue().toString());
                mavm.callApi(mavm.getGeo().getValue());
                obeserveAPI();
            }
        });
    }
}

 

 

MAgencyViewModel class

package wook.co.weather.viewmodels;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;

import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResponse;
import com.google.android.gms.location.SettingsClient;
import com.google.android.gms.tasks.OnSuccessListener;

import java.text.SimpleDateFormat;
import java.util.Calendar;

import wook.co.weather.models.dto.Coord;
import wook.co.weather.models.dto.GeoInfo;
import wook.co.weather.models.dto.GpsTransfer;
import wook.co.weather.models.dto.ShortWeather;
import wook.co.weather.models.repository.MAgencyRepo;
import wook.co.weather.view.MainActivity;
import wook.co.weather.view.splash.SplashActivity;

public class MAgencyViewModel extends AndroidViewModel { //ViewModel과 AndroidViewModel의 차이점은 Application의 유무이다.

    private final String TAG = "MAgencyViewModel";

    //이 클래스에서는 Model과 통신하여서 날씨 정보를 받아온다.
    private MutableLiveData<ShortWeather> sw;
    private MutableLiveData<GeoInfo> mldGi;
    private MAgencyRepo maRepo;
    private GeoInfo gi;
    private GpsTransfer gpt;

    private LocationCallback lcb;
    private LocationRequest lr;
    public boolean requestLocationUpdate;

    public MAgencyViewModel(@NonNull Application application) {
        super(application);
        Log.d(TAG,"LocationCallBack instance have been made");
        //LocationCallBack 부분 객체 생성하고 그에 LocatinResult 받았을때를 추상화 시켜주는 부분
        gpt = new GpsTransfer();
        lcb = new LocationCallback(){
            @Override
            public void onLocationResult(LocationResult locationResult){
                if(locationResult == null){ //locationResult를 받지 못했을떄 진입
                    Log.d(TAG,"Location information have not been recieved");
                    return;
                }
                Log.d(TAG,"Location information have been recieved");
                //gps를 통하여서 위도와 경도를 입력받는다.
                for (Location location : locationResult.getLocations()) {
                    if (location != null) {
                        gpt.setLon(location.getLongitude()); //경도를 입력 받는다.
                        gpt.setLat(location.getLatitude()); //위도를 입력 받는다.
                    }
                }

                //gps 연결을 닫는다.
                LocationServices.getFusedLocationProviderClient(getApplication()).removeLocationUpdates(lcb);

                //x,y 좌표로 변환
                gpt.transfer(gpt,0);
                Log.d(TAG, gpt.toString());
                setGeoInfo(gpt); //변환된 정보를 GeoInfo에 넣음
                mldGi.setValue(gi); //LiveData에 데이터를 입력한다.
            }
        };
    }

    //위치 정보 이용 권한 허가를 받지 못했을떄 호출 하는 부분
    public void defaultLocation() {

        //GpsTransfer 객체 생성
        gpt = new GpsTransfer();

        //GpsTransfer 위도와 경도를 원주로 설정
        gpt.setxLat(76);
        gpt.setyLon(122);
        gpt.transfer(gpt, 1);

        setGeoInfo(gpt);
        callApi(gi);

    }

    @SuppressLint("MissingPermission")//위치권한 체크안해도 된다고 하는 부분 안하는 이유는 SplashActivity에서 이미 했기 때문이다.
    public void requestUpdate(LocationRequest locationRequest){

        Log.d(TAG,"LocationRequest have been request");
        mldGi = new MutableLiveData<GeoInfo>();
        requestLocationUpdate = true;
        LocationServices.getFusedLocationProviderClient(getApplication())
                .requestLocationUpdates(locationRequest,lcb,null);
    }

    public void setGeoInfo(GpsTransfer gpt){
        gi = new GeoInfo();
        gi.setLon(gpt.getyLon());
        gi.setLat(gpt.getxLat());
        getTime();
    }

    public void callApi(GeoInfo geoInfo){
        if (sw != null) {
            return;
        }
        //해당 정보를 API를 호출
        maRepo = MAgencyRepo.getInStance();
        sw = maRepo.getWeather(geoInfo); // this part is calling a weather api
        Log.i(TAG, "API Connection finish");
    }

    public void getTime() {

        SimpleDateFormat dateSdf = new SimpleDateFormat("yyyyMMdd"); //년월일 받아오는 부분
        SimpleDateFormat timeSdf = new SimpleDateFormat("HH"); //현재시간 받아오는 부분

        Calendar cal = Calendar.getInstance(); //현재시간을 받아온다.

        gi.setNowDate(dateSdf.format(cal.getTime())); //날짜 세팅
        gi.setNowTime(timeSdf.format(cal.getTime())); //시간 세팅

        /*
         * 하루 전체의 기상예보를 받아오려면 전날 23시에 266개의 날씨정보를 호출해와야 한다.
         * 따라서 호출 값은 현재 날짜보다 1일전으로 세팅해줘야 한다.
         * */

        cal.add(Calendar.DATE, -1); //1일전 날짜를 구하기 위해 현재 날짜에서 -1 시켜주는 부분
        gi.setCallDate(dateSdf.format(cal.getTime())); //1일전 값으로 호출값 생성


        Log.i(TAG, "DATE : " + gi.getNowDate());
        Log.i(TAG, "TIME : " + gi.getNowTime());
        Log.i(TAG, "CALL DATE : " + gi.getCallDate());

    }

    public LiveData<ShortWeather> getWeather() {
        return sw;
    }
    public LiveData<GeoInfo> getGeo(){ return mldGi; }
}

 

 

참고 사이트

https://manorgass.tistory.com/82

 

Android :: 위치정보 파헤치기 / FusedLocationProvider 개념과 사용법

이번 포스팅에서는 Android의 위치 산출방법에 대해 살펴보고 매우 정확한 위치정보를 제공해주는 FusedLocationProvider의 개념과 사용법에 대해 이야기합니다. Android 기기의 위치정보를 획득하기 위

manorgass.tistory.com

https://www.youtube.com/watch?v=WuuZPPHOaVU&t=1304s&ab_channel=%EC%8A%AC%EA%B8%B0%EB%A1%9C%EC%9A%B4%EC%BD%94%EB%94%A9%EC%83%9D%ED%99%9C 

 

반응형