🤖 안드로이드 Android

[안드로이드/Android] 레트로핏 Retrofit 을 이용한 날씨 앱 만들기

핑크빛연어 2021. 8. 29. 17:14

 

Retrofit

 

REST API 통신을 위해 구현된 Squareup 사의 OkHttp 라이브러리의 상위 구현체로, OkHttp 를 네트워크 계층으로 활용하고 구축된 라이브러리.

AsyncTask 없이 Background Thread 를 실행하고 Callback 을 통해 Main Thread 에서 UI 업데이트가 가능하다.

 

 

🚨 장점

 

빠른 성능 - AsyncTask 를 사용하는 OkHttp 의 3배이상 차이가 난다고 한다.

가독성 - Annotation 사용으로 코드의 가독성이 뛰어남. 직관적인 설계가 가능.

간단한 구현 - HttpUrlConnection 의 Connection / Input&Output Stream / URL Encoding 등의 작업 또는 OkHttp 의 request / response 등의 작업을 라이브러리로 넘겨서 작업함.

동기/비동기 쉬운 구현 - response 를 받는 옵션으로 동기(execute()) / 비동기(enqueue(callback)) 가 있다. Background Thread 에서 request 를 수행한 후 callback 은 메인 스레드에서 처리.

 

 

🚨 구성요소

 

DTO - Model 클래스 사용 (JSON 타입변환에 사용)

Interface - 사용할 CRUD 동작 들을 정의해놓은 인터페이스

Retrofit.builder - Interface 를 사용할 인스턴스

 

 

🚨 Retrofit 예제 코드

 

안드로이드에서 서버와 통신하기 위한 retrofit 을 이용하는 예제 코드를 openweathermap 의 api 를 통해 날씨 정보를 표시해주는 앱으로 만들어 보았습니다. 🌤

https://openweathermap.org/api

 

Weather API - OpenWeatherMap

Please, sign up to use our fast and easy-to-work weather APIs for free. In case your requirements go beyond our freemium account conditions, you may check the entire list of our subscription plans. You can read the How to Start guide and enjoy using our po

openweathermap.org

 

 

작성한 파일 목록 입니다.

1. AndroidManifest.xml
2. build.gradle(:app)
3. strings.xml
4. WeatherInfoModel.java / WeatherWeatherModel.java / WeatherMainModel.java /
    WeatherWindModel.java / WeatherSysModel.java / WeatherCloudsModel.java
5. APIService.java
6. APIClient.java
7. MainActivity.java
8. activity_main.xml

 

 

1. AndroidManifest.xml

인터넷 권한 을 추가해야 합니다.
<uses-permission android:name="android.permission.INTERNET"/>
를 추가해주세요!

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.eun.myapp">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <activity android:name=".MainActivity">
        	<intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

 

 

2. build.gradle(:app)

하단 dependencies {} 안에 gson, glide, retrofit 에 대한 라이브러리를 사용하기 위해 추가해 줍니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.eun.myapp"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation 'com.android.support:design:28.0.0'

	//gson
    implementation 'com.google.code.gson:gson:2.8.0'

	//glide
    implementation 'com.github.bumptech.glide:glide:4.9.0'

	//retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.1.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
}

 

 

3. strings.xml

openweathermap.org 의 주소와 해당 사이트에서 발급받은 key값을 여기저기서 가져다 사용하기 위해 res/values/strings.xml 의 resources 에 지정해주었습니다.

<resources>
    <string name="app_name">MyApp</string>
    <string name="weather_url">http://api.openweathermap.org/</string>
    <string name="weather_app_id">key값</string>
</resources>

 

 

4. WeatherInfoModel.java

REST API 통신 시 받아올 데이터를 사용하기 쉽도록 응답받는 데이터에 맞추어 VO 를 구현한 Weather 관련 model class 입니당

JSON 을 사용할 경우 : @SerializedName("속성명") 으로 속성명 일치시켜주면 변수명 다르게도 가능

XML 을 사용할 경우 : @Element(name="속성명") XML은 @Element 어노테이션 사용

 

WeatherInfoModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

import java.util.List;

public class WeatherInfoModel {

    @SerializedName("name")
    String name = "";  //도시이름

    @SerializedName("weather")
    List<WeatherWeatherModel> weather;

    @SerializedName("main")
    WeatherMainModel main;

    @SerializedName("wind")
    WeatherWindModel wind;

    @SerializedName("sys")
    WeatherSysModel sys;

    @SerializedName("clouds")
    WeatherCloudsModel clouds;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<WeatherWeatherModel> getWeather() {
        return weather;
    }

    public void setWeather(List<WeatherWeatherModel> weather) {
        this.weather = weather;
    }

    public WeatherMainModel getMain() {
        return main;
    }

    public void setMain(WeatherMainModel main) {
        this.main = main;
    }

    public WeatherWindModel getWind() {
        return wind;
    }

    public void setWind(WeatherWindModel wind) {
        this.wind = wind;
    }

    public WeatherSysModel getSys() {
        return sys;
    }

    public void setSys(WeatherSysModel sys) {
        this.sys = sys;
    }

    public WeatherCloudsModel getClouds() {
        return clouds;
    }

    public void setClouds(WeatherCloudsModel clouds) {
        this.clouds = clouds;
    }
    
}

WeatherWeatherModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

public class WeatherWeatherModel {

    @SerializedName("main")
    String main = "";  //날씨

    @SerializedName("description")
    String description = "";   //상세 날씨 설명

    @SerializedName("icon")
    String icon = "";  //날씨 이미지

    public String getMain() {
        return main;
    }

    public void setMain(String main) {
        this.main = main;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }
    
}

WeatherMainModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

public class WeatherMainModel {

    @SerializedName("temp")
    double temp = 0.0;  //현재온도

    @SerializedName("humidity")
    double humidity = 0.0;  //현재습도

    public double getTemp() {
        return temp;
    }

    public void setTemp(double temp) {
        this.temp = temp;
    }

    public double getHumidity() {
        return humidity;
    }

    public void setHumidity(double humidity) {
        this.humidity = humidity;
    }
    
}

WeatherWindModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

public class WeatherWindModel {

    @SerializedName("speed")
    double speed = 0.0;

    public double getSpeed() {
        return speed;
    }

    public void setSpeed(double speed) {
        this.speed = speed;
    }

}

WeatherSysModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

public class WeatherSysModel {
    
    @SerializedName("country")
    String country = "";  //나라

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }
    
}

WeatherCloudsModel.java

package com.eun.myapp.data.retrofit.data;

import com.google.gson.annotations.SerializedName;

public class WeatherCloudsModel {

    @SerializedName("all")
    double all = 0.0;  //구름

    public double getAll() {
        return all;
    }

    public void setAll(double all) {
        this.all = all;
    }
    
}

 

 

5. APIService.java

Http Method(GET/POST/PUT/DELETE/HEAD) 와 자원의 정보를 정의할 인터페이스를 구현합니다. 
필요에 따라 변수를 포함하여 정의합니다.

 

@GET(EndPoint URL/{path}) ...

ex) http://eunoia3jy.tistory.com/main/123 에서 main/123 이 EndPoint URL 이 됩니다.

package com.eun.myapp.data.retrofit;

import com.eun.myapp.data.retrofit.data.WeatherInfoModel;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface APIService {
    
    @GET("data/2.5/{path}")
    Call<WeatherInfoModel> doGetJsonData(
            @Path("path") String path,
            @Query("q") String q,
            @Query("appid") String appid
    );
}

 

 

6. APIClient.java

retrofit 객체를 초기화합니다.

baseUrl() 안의 url 은 꼭 / 로 끝나야 합니다. 아니면 예외 발생이 됩니다.

package com.eun.myapp.data.retrofit;

import android.util.Log;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class APIClient {
    private static final String TAG = APIClient.class.getSimpleName();
    
    public static Retrofit getClient(String url) {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        Log.d(TAG, "APIClient >> url : "+url);

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create())
                .client(client)
                .build();

        return retrofit;
    }
}

 

 

7.  MainActivity.java

화면을 구성하고 네트워크 통신하는 과정을 나타냅니다.

retrofit 객체와 인터페이스를 연결하고 네트워크 통신 요청 및 응답 콜백을 구현합니다.

response 를 받는 옵션으로 동기(execute()) / 비동기(enqueue(callback)) 가 있습니다.

Background Thread 에서 request 를 수행한 후 callback 은 메인 스레드에서 처리합니다.

onResponse() 에서는 실패코드, 성공코드 모두 호출 할 수 있기 때문에 isSuccessful() 로 확인이 필요합니다.

package com.eun.myapp;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.eun.myapp.data.retrofit.APIClient;
import com.eun.myapp.data.retrofit.APIService;
import com.eun.myapp.data.retrofit.data.WeatherInfoModel;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {
    public static String TAG = "["+MainActivity.class.getSimpleName() +"] ";
    Context context = MainActivity.this;

    TextView tv_name, tv_country;
    ImageView iv_weather;
    TextView tv_temp, tv_main, tv_description;
    TextView tv_wind, tv_cloud, tv_humidity;

    APIService apiInterface = null;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        requestNetwork();
    }


    /* view 를 설정하는 메소드 */
    private void initView() {
        tv_name = (TextView) findViewById(R.id.tv_name);
        tv_country = (TextView) findViewById(R.id.tv_country);
        iv_weather = (ImageView) findViewById(R.id.iv_weather);
        tv_temp = (TextView) findViewById(R.id.tv_temp);
        tv_main = (TextView) findViewById(R.id.tv_main);
        tv_description = (TextView) findViewById(R.id.tv_description);
        tv_wind = (TextView) findViewById(R.id.tv_wind);
        tv_cloud = (TextView) findViewById(R.id.tv_cloud);
        tv_humidity = (TextView) findViewById(R.id.tv_humidity);
    }


    /* retrofit 을 통해 통신을 요청하기 위한 메소드 */
    private void requestNetwork() {

        //retrofit 객체와 인터페이스 연결
        apiInterface = APIClient.getClient(getString(R.string.weather_url)).create(APIService.class);

        //통신 요청
        Call<WeatherInfoModel> call = apiInterface.doGetJsonData("weather", "seoul", getString(R.string.weather_app_id));

        //응답 콜백 구현
        call.enqueue(new Callback<WeatherInfoModel>() {

            @Override
            public void onResponse(Call<WeatherInfoModel> call, Response<WeatherInfoModel> response) {
                WeatherInfoModel resource = response.body();
                if(response.isSuccessful()) {
                    setWeatherData(resource);  //UI 업데이트
                } else {
                    showFailPop();
                }
            }

            @Override
            public void onFailure(Call<WeatherInfoModel> call, Throwable t) {
                call.cancel();
                showFailPop();
            }

        });

    }


    /* 통신하여 받아온 날씨 데이터를 통해 UI 업데이트 메소드 */
    private void setWeatherData(WeatherInfoModel model) {
        tv_name.setText(model.getName());
        tv_country.setText(model.getSys().getCountry());
        Glide.with(context).load(getString(R.string.weather_url)+"img/w/"+model.getWeather().get(0).getIcon()+".png")  //Glide 라이브러리를 이용하여 ImageView 에 url 로 이미지 지정
                .placeholder(R.drawable.icon_image)
                .error(R.drawable.icon_image)
                .into(iv_weather);
        tv_temp.setText(doubleToStrFormat(2, model.getMain().getTemp()-273.15) + " 'C");  //소수점 2번째 자리까지 반올림하기
        tv_main.setText(model.getWeather().get(0).getMain());
        tv_description.setText(model.getWeather().get(0).getDescription());
        tv_wind.setText(doubleToStrFormat(2, model.getWind().getSpeed()) + " m/s");
        tv_cloud.setText(doubleToStrFormat(2, model.getClouds().getAll()) + " %");
        tv_humidity.setText(doubleToStrFormat(2, model.getMain().getHumidity()) + " %");
    }


    /* 통신 실패시 AlertDialog 표시하는 메소드 */
    private void showFailPop() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Title").setMessage("통신실패");

        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                Toast.makeText(getApplicationContext(), "OK Click", Toast.LENGTH_SHORT).show();
            }
        });

        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                Toast.makeText(getApplicationContext(), "Cancel Click", Toast.LENGTH_SHORT).show();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }


    /* 소수점 n번째 자리까지 반올림하기 */
    private String doubleToStrFormat(int n, double value) {
        return String.format("%."+n+"f", value);
    }


}

 

 

8. activity_main.xml

MainActivity 에 대한 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
    android:background="@color/white"
    tools:context=".ConnectActivity" >


    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/amber_200"
        android:elevation="10dp"
        android:gravity="center"
        android:text="현재 날씨"
        android:textColor="@color/black"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/ll_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <LinearLayout
        android:id="@+id/ll_name"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginTop="50dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginBottom="20dp"
        android:weightSum="100"
        android:orientation="horizontal"
        android:background="@drawable/bg_custom_title_name"
        android:gravity="center"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_title" >
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="50"
            android:layout_marginRight="2dp"
            android:gravity="right|center_vertical"
            android:text="Seoul"
            android:textStyle="bold"
            android:textColor="@color/white"
            android:textSize="20dp" />
        <TextView
            android:id="@+id/tv_country"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="50"
            android:layout_marginLeft="2dp"
            android:gravity="left|center_vertical"
            android:text="KR"
            android:textStyle="bold"
            android:textColor="@color/white"
            android:textSize="20dp" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/ll_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="30dp"
        android:layout_marginHorizontal="20dp"
        android:orientation="horizontal"
        android:weightSum="100"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ll_name" >

        <ImageView
            android:id="@+id/iv_weather"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="30"
            android:src="@drawable/icon_image"
            android:background="@drawable/bg_solid" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_weight="70"
            android:gravity="center_vertical"
            android:orientation="vertical"
            android:weightSum="100"
            app:layout_constraintLeft_toRightOf="@+id/iv_weather"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" >
            <TextView
                android:id="@+id/tv_temp"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="35"
                android:gravity="center_vertical"
                android:paddingLeft="25dp"
                android:text="12 C"
                android:textColor="@color/blue_light"
                android:textSize="25sp"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/tv_main"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="35"
                android:gravity="center_vertical"
                android:paddingLeft="25dp"
                android:text="Clear Sky"
                android:textColor="@color/black"
                android:textSize="20sp"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/tv_description"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="30"
                android:gravity="center_vertical"
                android:paddingLeft="25dp"
                android:text="broken clouds"
                android:textColor="@color/grey_500"
                android:textSize="17sp" />
        </LinearLayout>

    </LinearLayout>


    <View
        android:id="@+id/view_divider"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginVertical="30dp"
        android:layout_marginHorizontal="20dp"
        android:background="@color/grey_200"
        app:layout_constraintBottom_toTopOf="@+id/ll_detail"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ll_main" />


    <LinearLayout
        android:id="@+id/ll_detail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="30dp"
        android:layout_marginHorizontal="20dp"
        android:orientation="horizontal"
        android:weightSum="100"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/view_divider" >

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="33"
            android:orientation="vertical"
            android:weightSum="100" >
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="45"
                android:paddingTop="5dp"
                android:src="@drawable/icon_wind" />
            <TextView
                android:id="@+id/nm_wind"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="15"
                android:gravity="center"
                android:text="바람  "
                android:textColor="@color/grey_500"
                android:textSize="13sp" />
            <TextView
                android:id="@+id/tv_wind"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="40"
                android:gravity="center_horizontal"
                android:text="4.6m/s"
                android:textColor="@color/black"
                android:textSize="17sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="33"
            android:orientation="vertical"
            android:weightSum="100" >
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="45"
                android:paddingTop="5dp"
                android:src="@drawable/icon_cloud" />
            <TextView
                android:id="@+id/nm_cloud"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="15"
                android:gravity="center"
                android:text="구름  "
                android:textColor="@color/grey_500"
                android:textSize="13sp" />
            <TextView
                android:id="@+id/tv_cloud"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="40"
                android:gravity="center_horizontal"
                android:text="75%"
                android:textColor="@color/black"
                android:textSize="17sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="33"
            android:orientation="vertical"
            android:weightSum="100" >
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="45"
                android:gravity="center"
                android:paddingTop="5dp"
                android:src="@drawable/icon_humidity" />
            <TextView
                android:id="@+id/nm_humidity"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="15"
                android:gravity="center"
                android:text="습도  "
                android:textColor="@color/grey_500"
                android:textSize="13sp" />
            <TextView
                android:id="@+id/tv_humidity"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="40"
                android:gravity="center_horizontal"
                android:text="59%"
                android:textColor="@color/black"
                android:textSize="17sp" />
        </LinearLayout>

    </LinearLayout>


</android.support.constraint.ConstraintLayout>

 

 

결과 화면

 

 

감사합니당!

728x90
반응형