일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- ar vr 차이
- 자바 다형성
- 2022 플러터 안드로이드 스튜디오
- 큐 자바 코드
- 안드로이드 레트로핏 crud
- ANR이란
- jvm이란
- 스택 자바 코드
- android retrofit login
- 안드로이드 라이선스
- android ar 개발
- 안드로이드 라이선스 종류
- rxjava cold observable
- rxjava hot observable
- 서비스 쓰레드 차이
- rxjava disposable
- 서비스 vs 쓰레드
- 2022 플러터 설치
- 객체
- 스택 큐 차이
- 플러터 설치 2022
- jvm 작동 원리
- 안드로이드 레트로핏 사용법
- 안드로이드 유닛 테스트
- 안드로이드 유닛 테스트 예시
- 안드로이드 os 구조
- 멤버변수
- Rxjava Observable
- 클래스
- 안드로이드 유닛테스트란
- Today
- Total
나만을 위한 블로그
[Android] 레트로핏을 이용한 회원가입, 로그인 기능 구현 (with MySQL, PHP) 본문
※ 아래 내용은 예제 수준의 내용이기 때문에 커스텀하기 전에 코드를 반드시 이해한 다음 쓰자.
2020.12.12 - 모든 코드 정상 작동 확인
예전에 Volley를 통해 회원가입, 로그인 기능을 구현하는 포스팅을 쓴 적이 있다.
onlyfor-me-blog.tistory.com/119
이번 포스팅에선 Volley가 아닌 레트로핏을 사용해서 회원가입, 로그인 기능을 구현하는 예제를 포스팅하려 한다.
먼저 DB 구조는 아래와 같다. DB명은 my_app으로 지었다.
이렇게 만든 다음 PHP 파일을 만든다. 각 파일의 내용은 아래와 같다. PHP 파일명은 주석으로 써뒀으니 참고해서 작성할 때 사용하자.
<?php
// retrofit_config.php
$host = "xx.xxx.xxx.xx";
$user = "root";
$password = "비밀번호";
$db = "my_app";
$con = mysqli_connect($host, $user, $password, $db);
if ($con)
{
echo "접속 성공";
}
else
{
echo "접속 실패";
}
config 파일은 DB에 연결하기 위해 사용되는 PHP 파일이다. 자신의 서버(xampp를 사용 중이라면 localhost를 적는다) 주소, DB 비밀번호를 자신의 것으로 바꿔 입력한 뒤, 웹 브라우저 주소창에 이 파일의 주소를 입력하면 접속 성공 여부에 따라 접속했는지 실패했는지를 알 수 있다.
이 파일을 웹 브라우저에서 테스트할 때 만약 한글이 깨지는 현상이 발생한다면 파일의 문자 인코딩을 UTF-8로 설정했는지 확인하자. 비주얼 스튜디오 코드를 쓴다면 하단에 인코딩 형식이 EUC-KR로 되어있을 수 있는데, 이것이나 다른 형태로 되어 있다면 UTF-8로 인코딩을 바꿔 저장해주면 문제없이 한글이 출력된다.
<?php
// retrofit_simpleregister.php
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
include_once("retrofit_config.php");
$name = $_POST['name'];
$username = $_POST['username'];
$password = $_POST['password'];
$hobby= $_POST['hobby'];
if($name == '' || $username == '' || $password == '' || $hobby == '')
{
echo json_encode(array(
"status" => "false",
"message" => "필수 인자가 부족합니다")
);
}
else
{
$query= "SELECT * FROM retrofitRegister WHERE username='$username'";
$result= mysqli_query($con, $query);
if(mysqli_num_rows($result) > 0){
echo json_encode(array( "status" => "false","message" => "이미 존재하는 이름입니다") );
}
else
{
$query = "INSERT INTO retrofitRegister (name,hobby,username,password) VALUES ('$name','$hobby','$username','$password')";
if(mysqli_query($con,$query))
{
$query= "SELECT * FROM retrofitRegister WHERE username='$username'";
$result= mysqli_query($con, $query);
$emparray = array();
if(mysqli_num_rows($result) > 0)
{
while ($row = mysqli_fetch_assoc($result))
{
$emparray[] = $row;
}
}
echo json_encode(
array(
"status" => "true",
"message" => "회원가입 성공",
"data" => $emparray)
);
}
else
{
echo json_encode(
array(
"status" => "false",
"message" => "에러가 발생했습니다. 다시 시도해 주세요"
)
);
}
}
mysqli_close($con);
}
}
위에서 작성한 config 파일을 써서 INSERT문으로 테이블에 데이터를 넣거나, SELECT문으로 앱에서 입력한 username을 조회해 이미 존재한다면 그것을 알려주는 PHP 파일이다.
각 쿼리가 실행된 후에는 앱으로 보낼 메시지들을 배열에 넣은 뒤 이것을 JSON 형태로 보낸다.
<?php
// retrofit_simplelogin.php
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
include_once("retrofit_config.php");
$username = $_POST['username'];
$password = $_POST['password'];
if( $username == '' || $password == '')
{
echo json_encode(array(
"status" => "false",
"message" => "Parameter missing!"
));
}
else
{
$query= "SELECT * FROM retrofitRegister WHERE username='$username' AND password='$password'";
$result= mysqli_query($con, $query);
if(mysqli_num_rows($result) > 0)
{
$query= "SELECT * FROM retrofitRegister WHERE username='$username' AND password='$password'";
$result= mysqli_query($con, $query);
$emparray = array();
if(mysqli_num_rows($result) > 0)
{
while ($row = mysqli_fetch_assoc($result))
{
$emparray[] = $row;
}
}
echo json_encode(
array(
"status" => "true",
"message" => "로그인 성공",
"data" => $emparray
)
);
}
else
{
echo json_encode(
array(
"status" => "false",
"message" => "아이디 또는 비밀번호를 확인해 주세요")
);
}
mysqli_close($con);
}
}
else
{
echo json_encode(
array(
"status" => "false",
"message" => "에러가 발생했습니다. 다시 시도해 주세요"
)
);
}
앱에서 입력한 username, password를 통해 로그인 처리를 수행하는 PHP 파일이다.
위의 retrofit_register.php 파일과 비슷한 내용이다.
이제 레트로핏을 써보자. 먼저 앱 수준 gradle에 아래 의존성 문장들을 추가해준다.
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
implementation 'com.squareup.retrofit2:converter-gson:2.6.4'
implementation 'com.squareup.retrofit2:converter-scalars:2.6.4'
그리고 매니페스트에도 인터넷 권한을 명시해준다. 인터넷으로 웹 서버에 접근해야 하니까 당연한 처리다.
<uses-permission android:name="android.permission.INTERNET" />
추가로 application 태그 안의 속성으로 아래 속성을 추가한다.
android:usesCleartextTraffic="true"
다음은 앱 화면과 자바 파일을 코딩하기 전 먼저 만들어야 하는 파일들이다.
먼저 쉐어드를 사용하기 때문에 쉐어드 관련 메서드들을 모아놓은 헬퍼 클래스다.
import android.content.Context;
import android.content.SharedPreferences;
public class PreferenceHelper
{
private final String INTRO = "intro";
private final String NAME = "name";
private final String HOBBY = "hobby";
private SharedPreferences app_prefs;
private Context context;
public PreferenceHelper(Context context)
{
app_prefs = context.getSharedPreferences("shared", 0);
this.context = context;
}
public void putIsLogin(boolean loginOrOut)
{
SharedPreferences.Editor edit = app_prefs.edit();
edit.putBoolean(INTRO, loginOrOut);
edit.apply();
}
public void putName(String loginOrOut)
{
SharedPreferences.Editor edit = app_prefs.edit();
edit.putString(NAME, loginOrOut);
edit.apply();
}
public String getName()
{
return app_prefs.getString(NAME, "");
}
public void putHobby(String loginOrOut)
{
SharedPreferences.Editor edit = app_prefs.edit();
edit.putString(HOBBY, loginOrOut);
edit.apply();
}
public String getHobby()
{
return app_prefs.getString(HOBBY, "");
}
}
다음은 레트로핏을 쓰는 데 꼭 필요한 RegisterInterface, LoginInterface란 이름의 인터페이스다.
하나의 인터페이스에 회원가입과 로그인을 처리하는 레트로핏 메서드를 만들어도 되지만 2개로 나눴다.
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface RegisterInterface
{
String REGIST_URL = "http://xx.xxx.xxx.xx/";
@FormUrlEncoded
@POST("retrofit_simpleregister.php")
Call<String> getUserRegist(
@Field("name") String name,
@Field("hobby") String hobby,
@Field("username") String username,
@Field("password") String password
);
}
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface LoginInterface
{
String LOGIN_URL = "http://xx.xxx.xxx.xx/";
@FormUrlEncoded
@POST("retrofit_simplelogin.php")
Call<String> getUserLogin(
@Field("username") String username,
@Field("password") String password
);
}
각 String 변수에는 자신의 서버 주소를 입력한다. 마찬가지로 xampp를 쓴다면 http://localhost/를 입력하면 된다.
여기서 정의한 메서드를 통해 PHP 파일에 접근해서 인자들을 PHP 파일로 전달하고, PHP 파일은 DB에 접근해 데이터를 추가/조회해서 회원가입, 로그인 처리를 수행하게 된다.
@Field는 POST로 서버에 값을 보낼 때 붙여야 하는 어노테이션이다. 또한 @Field() 안의 큰따옴표 안에는 PHP 파일에서 $_POST['username']; 의 [''] 안에 있는 것과 똑같은 이름을 넣어야 한다. 다른 이름을 넣으면 앱이 뻑날 수 있다.
다른 어노테이션들의 의미를 모른다면 구글링 ㄱㄱ
다음은 앱 코드다. 앱 화면은 회원가입을 처리하는 MainActivity, 로그인을 처리하는 LoginActivity 2가지다.
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="레트로핏 회원가입"
android:textColor="#fff"
android:textSize="29sp" />
<EditText
android:id="@+id/etname"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="아이디"
android:paddingLeft="5dp" />
<EditText
android:id="@+id/ethobby"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="Enter Hobby"
android:paddingLeft="5dp" />
<EditText
android:id="@+id/etusername"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="유저 이름"
android:paddingLeft="5dp" />
<EditText
android:id="@+id/etpassword"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="비밀번호"
android:inputType="textPassword"
android:paddingLeft="5dp" />
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="@color/colorPrimary"
android:text="회원가입"
android:textColor="#fff" />
<TextView
android:id="@+id/tvlogin"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="이미 아이디가 있으시다면 여길 눌러 로그인하세요"
android:textColor="#fff"
android:textSize="20sp" />
</LinearLayout>
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class MainActivity extends AppCompatActivity
{
public final String TAG = "MainActivity";
private EditText etname, ethobby, etusername, etpassword;
private Button btnregister;
private TextView tvlogin;
private PreferenceHelper preferenceHelper;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
preferenceHelper = new PreferenceHelper(this);
etname = (EditText) findViewById(R.id.etname);
ethobby = (EditText) findViewById(R.id.ethobby);
etusername = (EditText) findViewById(R.id.etusername);
etpassword = (EditText) findViewById(R.id.etpassword);
btnregister = (Button) findViewById(R.id.btn);
tvlogin = (TextView) findViewById(R.id.tvlogin);
tvlogin.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
finish();
}
});
btnregister.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
registerMe();
}
});
}
private void registerMe()
{
final String name = etname.getText().toString();
final String hobby = ethobby.getText().toString();
final String username = etusername.getText().toString();
final String password = etpassword.getText().toString();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(RegisterInterface.REGIST_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.build();
RegisterInterface api = retrofit.create(RegisterInterface.class);
Call<String> call = api.getUserRegist(name, hobby, username, password);
call.enqueue(new Callback<String>()
{
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response)
{
if (response.isSuccessful() && response.body() != null)
{
Log.e("onSuccess", response.body());
String jsonResponse = response.body();
try
{
parseRegData(jsonResponse);
}
catch (JSONException e)
{
e.printStackTrace();
}
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t)
{
Log.e(TAG, "에러 = " + t.getMessage());
}
});
}
private void parseRegData(String response) throws JSONException
{
JSONObject jsonObject = new JSONObject(response);
if (jsonObject.optString("status").equals("true"))
{
saveInfo(response);
Toast.makeText(MainActivity.this, "회원가입 성공", Toast.LENGTH_SHORT).show();
}
else
{
Toast.makeText(MainActivity.this, jsonObject.getString("message"), Toast.LENGTH_SHORT).show();
}
}
private void saveInfo(String response)
{
preferenceHelper.putIsLogin(true);
try
{
JSONObject jsonObject = new JSONObject(response);
if (jsonObject.getString("status").equals("true"))
{
JSONArray dataArray = jsonObject.getJSONArray("data");
for (int i = 0; i < dataArray.length(); i++)
{
JSONObject dataobj = dataArray.getJSONObject(i);
preferenceHelper.putName(dataobj.getString("name"));
preferenceHelper.putHobby(dataobj.getString("hobby"));
}
}
}
catch (JSONException e)
{
e.printStackTrace();
}
}
}
<!-- activity_login.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="#000"
android:orientation="vertical">
<EditText
android:id="@+id/etusername"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="아이디"
android:paddingLeft="5dp" />
<EditText
android:id="@+id/etpassword"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="#fff"
android:hint="비밀번호"
android:inputType="textPassword"
android:paddingLeft="5dp" />
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
android:background="@color/colorPrimary"
android:text="로그인"
android:textColor="#fff" />
<TextView
android:id="@+id/tvreg"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="회원가입은 여기로"
android:textColor="#fff"
android:textSize="20sp" />
</LinearLayout>
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class LoginActivity extends AppCompatActivity
{
private final String TAG = "LoginActivity";
private EditText etUname, etPass;
private Button btnlogin;
private TextView tvreg;
private PreferenceHelper preferenceHelper;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
preferenceHelper = new PreferenceHelper(this);
etUname = (EditText) findViewById(R.id.etusername);
etPass = (EditText) findViewById(R.id.etpassword);
btnlogin = (Button) findViewById(R.id.btn);
tvreg = (TextView) findViewById(R.id.tvreg);
tvreg.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
LoginActivity.this.finish();
}
});
btnlogin.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
loginUser();
}
});
}
private void loginUser()
{
final String username = etUname.getText().toString().trim();
final String password = etPass.getText().toString().trim();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(LoginInterface.LOGIN_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.build();
LoginInterface api = retrofit.create(LoginInterface.class);
Call<String> call = api.getUserLogin(username, password);
call.enqueue(new Callback<String>()
{
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response)
{
if (response.isSuccessful() && response.body() != null)
{
Log.e("onSuccess", response.body());
String jsonResponse = response.body();
parseLoginData(jsonResponse);
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable t)
{
Log.e(TAG, "에러 = " + t.getMessage());
}
});
}
private void parseLoginData(String response)
{
try
{
JSONObject jsonObject = new JSONObject(response);
if (jsonObject.getString("status").equals("true"))
{
saveInfo(response);
Toast.makeText(LoginActivity.this, "Login Successfully!", Toast.LENGTH_SHORT).show();
}
}
catch (JSONException e)
{
e.printStackTrace();
}
}
private void saveInfo(String response)
{
preferenceHelper.putIsLogin(true);
try
{
JSONObject jsonObject = new JSONObject(response);
if (jsonObject.getString("status").equals("true"))
{
JSONArray dataArray = jsonObject.getJSONArray("data");
for (int i = 0; i < dataArray.length(); i++)
{
JSONObject dataobj = dataArray.getJSONObject(i);
preferenceHelper.putName(dataobj.getString("name"));
preferenceHelper.putHobby(dataobj.getString("hobby"));
}
}
}
catch (JSONException e)
{
e.printStackTrace();
}
}
}
각 자바 파일을 보면 JSONException을 처리하는 걸 볼 수 있다.
서버에서 JSON 형태로 값이 날아오기 때문인데, 이걸 처리하는 방법은 try-catch문을 사용하거나 메서드의 () 뒤에 throws JSONException을 써주는 2가지 방법이 있다. 둘 중 하나를 선택하면 된다.
위 처리를 했다면 JSONObject에 response.body()를 담아서 getString()으로 JSON 문자열 안의 Key 값을 빼온 다음, 그것의 Value를 통해 회원가입/로그인 성공 여부를 확인해 토스트로 출력해서 확인한다. 토스트가 안 나오더라도 로그를 통해서 회원가입/로그인 여부를 확인하면 된다.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(LoginInterface.LOGIN_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.build();
이 부분은 따로 클래스로 떼어내서 만들어도 된다. 클래스로 만든다면 아래와 같이 만들 수 있다.
public class ApiClient {
private static final String BASE_URL = "http://xx.xxx.xxx.xx/";
private static Retrofit retrofit;
public static Retrofit getApiClient()
{
Gson gson = new GsonBuilder()
.setLenient()
.create();
if (retrofit == null)
{
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(new NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit;
}
}
static으로 만들었기 때문에 ApiClient를 입력하고 도트(.)를 입력하면 바로 getApiClient()가 나온다. 그리고 다시 도트를 입력하면 create()가 나오기 때문에, 도트와 엔터를 빠르게 번갈아 치면 제법 편하게 코드를 칠 수 있다.
이렇게 한 다음 액티비티에서 사용하고 인터페이스를 호출하는 법은 아래처럼 할 수 있다.
ApiInterface apiInterface = ApiClient.getApiClient().create(ApiInterface.class);
Call<String> call = apiInterface.methodName();
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response)
{
if (response.isSuccessful() && response.body() != null)
{
getResults(response.body());
}
}
@Override
public void onFailure(Call<String> call, Throwable t)
{
damnError(t.getLocalizedMessage());
}
});
위 과정들을 따라왔다면 앱을 실행한 후 회원가입을 진행한 뒤 로그인을 하면 로그캣에 아래와 같은 로그가 찍힌다.
그리고 DB에도 아래처럼 값이 저장돼 있는 걸 볼 수 있다.
서버에서 JSON 형태로 날아오는 값을 파싱해 사용하는 방법은 편하기도 해서 자주 쓰이는 방법이니, 파싱하는 방법은 꼭 알아두자.
쉐어드를 쓸 필요가 없겠다 싶으면 액티비티에서 사용된 헬퍼 클래스의 메서드들만 지우면 된다.
'Android' 카테고리의 다른 글
[Android] 플레이 스토어의 키스토어 분실 시 재설정하는 방법 (0) | 2021.01.18 |
---|---|
[Android] 리사이클러뷰 아이템 안의 체크박스의 체크 상태를 유지시키는 법 (0) | 2020.12.24 |
[Android] 특정 상황에 FCM 푸시 메시지를 받지 않도록 설정하는 법 (0) | 2020.12.09 |
[Android] SQLite 사용법 - INSERT - (0) | 2020.12.07 |
[Android] editText 바깥 부분을 클릭하면 키보드 내려가게 하는 법 (0) | 2020.11.30 |