728x90

구글에서 제공하는 함수를 이용하여 거리를 계산하는 걸 시도했으나 실패를 했다.

API_KEY를 새로 생성해서도 해보고 기존 API를 이용해서도 해보았으나, 역시 잘 안된다.




위와 같이 설정하면 얼추 동작이 되는 거 같기도 하다.



private void getMapDistanceData(String latitude, String longitude) {
    // 1. RequestQueue 생성 및 초기화
    RequestQueue requestQueue = Volley.newRequestQueue(mContext);
    String url = "https://maps.googleapis.com/maps/api/distancematrix/json?units=metric&mode=transit&origins=37.541,126.986&destinations=35.1595454,126.8526012&region=KR&key=API_KEY";

    // 2. Request Obejct인 StringRequest 생성
    StringRequest request = new StringRequest(Request.Method.POST, url,
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d("result", "[" + response + "]");
                    showJSONList(response);
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d("error", "[" + error.getMessage() + "]");
                }
            }) {
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            Map<String, String> params = new HashMap<>();
            params.put("keyword", Value.encrypt(Value.URLkey()));
            params.put("reg_date", Value.encrypt(Value.Today()));
            return params;
        }
    };

    // 3) 생성한 StringRequest를 RequestQueue에 추가
    requestQueue.add(request);
}
 


두 좌표(위도, 경도)간의 거리결과를 얻는 것이 목적이라 위 코드는 포기하고 PHP 코드를 검색해서 만들어서 사용했더니 만족스런 결과가 나온다.

function getDistance($lat1, $lng1, $lat2, $lng2) { // 위, 경도 거리 계산
    $earth_radius = 6371;
    $dLat = deg2rad($lat2 - $lat1);
    $dLon = deg2rad($lng2 - $lng1);
    $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2) * sin($dLon/2);
    $c = 2 * asin(sqrt($a));
    $d = $earth_radius * $c;
    return $d * 1000; // m 거리 반환
    //return $d; // km 거리 반환
}

function StDistance($StID, $lat1, $lng1){
    if(strlen($lat1) < 1 || strlen($lng1) < 1) {
        return "X";
    }
    $sql = "select latitude,longitude from Station where StID=?";
    $params = array($StID);
    $stmt = $this->db->prepare($sql);
    $stmt->execute($params);
    if($row = $stmt->fetch()){
        return $this->getDistance($lat1, $lng1, $row[0], $row[1]);
    } else {
        return "X";
    }
}


DB에 저장된 위도, 경도값과 현재 위치에서 반환한 위도, 경도 값의 차이를 기준으로 거리 계산을 하도록 하면 원하는 결과를 얻을 수 있다.


블로그 이미지

Link2Me

,
728x90

HTML 을 PDF로 출력하기 위해 테스트하고 간략하게 적어둔다.


###### TCPDF 설치 ############
# 1. 웹 브라우저에서 https://sourceforge.net/projects/tcpdf/files/ 에 접속하여 TCPDF를 다운받는다.
# 현재 기준 tcpdf_6_3_2.zip 가 가장 최신이다.
# 다운로드 받은 파일을 리눅스 /usr/share/php 경로에 업로드 한다.

# 다른 경로에 설치해도 되는데 나눔폰트 설치 경로를 변경하기 싫어서 이곳 저곳 설치해보고 그냥 이 폴더로 정했다.
cd /usr/share/php
unzip tcpdf_6_3_2.zip

# 나눔폰트 설치 방법
cd /usr/share/fonts/
wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip

unzip NanumFont_TTF_ALL.zip -d NanumFont
cd /usr/share/php/tcpdf/tools/
ls /usr/share/fonts/NanumFont/*
ls /usr/share/fonts/NanumFont/* | xargs -I TTF php tcpdf_addfont.php -i TTF
ll /usr/share/php/tcpdf/fonts/ | grep nanum

# 예제파일을 참조하여 실행할 php 디렉토리에서 내용을 구현한다.

먼저 화면상으로 원하는 형태로 출력이 되는지 확인하고 최종적으로 PDF 파일로 생성되도록 하면 된다.


tcpdf_include.php 파일에서 경로를 수정해준다.

$tcpdf_include_dirs = array(
    realpath('/usr/share/php/tcpdf/tcpdf.php'),
    '/usr/share/php/tcpdf/tcpdf.php'
);

<?php
// Include the main TCPDF library (search for installation path).
require_once('tcpdf_include.php');

// Set some content to print
$html = <<<EOD
<h3>반갑습니다. PDF센터에 오신 걸 환영합니다.</h3>
<i>This is the first example of TCPDF library.</i>
EOD;

// create new PDF document
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);

// set document information
$pdf->SetCreator(PDF_CREATOR);

// set margins
$pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
$pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
$pdf->SetFooterMargin(PDF_MARGIN_FOOTER);

// Set font
//$pdf->SetFont('nanumgothic', '', 14, '', true);
$pdf->SetFont('nanumgothic');
$pdf->SetDefaultMonospacedFont("nanumgothic_coding");

// Add a page
$pdf->AddPage();

// Print text using writeHTMLCell()
$pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true);

// Close and output PDF document
//$pdf->Output(getcwd().'/001.pdf', 'F'); // 실제 파일 생성됨
$pdf->Output('001.pdf', 'I'); // 화면으로 보여줌
?>
 



참조 사이트

https://wickedmagic.tistory.com/571

https://zetawiki.com/wiki/TCPDF_%ED%95%9C%EA%B8%80_%ED%8F%B0%ED%8A%B8_%EC%82%AC%EC%9A%A9

'안드로이드 > Android QRCode' 카테고리의 다른 글

Android QRCode 샘플  (0) 2020.08.25
PHP QR code 생성하는 방법  (0) 2020.03.04
QRCode 개요  (0) 2020.03.03
블로그 이미지

Link2Me

,
728x90

특정 문자열을 파싱처리하는 걸 처리하는데 동일한 값이 두번 나온다고 한다.

그래서 두번째 값을 읽어낸다고 한다.

한번만 해당 정보가 출력되는 줄 알았는데 동일한 정보가 여러개 있을 수 있는 환경이다.


String gw_alloc;

private void parseDeviceIPAddress(String msg){
    String[] line = msg.split("\\n"); // 라인 단위로 메시지를 분리하라.
    for(int i=0; i < line.length;i++){
        if(line[i].contains("GW=")){  // GW address 추출
            String str = line[i].trim(); // 문자열 앞의 공백 제거 목적
            int stlocation = str.indexOf("GW=");
            int endlocation = str.length();
            String temp = str.substring(stlocation+3, endlocation-1).trim();
            int firstcomma = temp.indexOf(","); // 처음 나오는 , 를 찾아라.
            String gwaddress = temp.substring(0,firstcomma).trim();
            if(gwaddress.length() > 8){
                gw_alloc = gwaddress;
            }
        }
    }
}


그래서 첫번째 값을 읽어내도록 하는 걸 처리하기 위해서 코드를 아래와 같이 ArrayList를 이용해서 수정했다.

ArrayList<String> gw_alloc = new ArrayList<>();

private void parseDeviceIPAddress(String msg){
    String[] line = msg.split("\\n"); // 라인 단위로 메시지를 분리하라.
    for(int i=0; i < line.length;i++){
        if(line[i].contains("GW=")){  // GW address 추출
            String str = line[i].trim(); // 문자열 앞의 공백 제거 목적
            int stlocation = str.indexOf("GW=");
            int endlocation = str.length();
            String temp = str.substring(stlocation+3, endlocation).trim();
            int firstcomma = temp.indexOf(","); // 처음 나오는 , 를 찾아라.
            String gwaddress = temp.substring(0,firstcomma).trim();
            if(gwaddress.length() > 8){
                gw_alloc.add(gwaddress);
            }
        }
    }
}


해당 정보가 있으면 ArrayList 에 add하고 출력할 때는 gw_alloc.get(0); 를 해주면 첫번째 값을 활용할 수 있다.

블로그 이미지

Link2Me

,
728x90

SharedPreferences 싱글톤 처리 코드이다.

SharedPreferences는 간단한 값을 저장할 때 주로 사용한다.
초기 설정 값이나 자동 로그인 여부 등 간단한 값을 저장할 때 DB를 사용하면 복잡하기 때문에 SharedPreferences를 사용하면 적합하다.
SharedPreferences는 어플리케이션에 파일 형태로 데이터를 저장한다. 데이터는 (key, value) 형태로 data/data/패키지명/shared_prefs 폴더 안에 xml 파일로 저장된다.
해당 파일은 어플리케이션이 삭제되기 전까지 보존된다.
SharedPreferences는 앱의 어디서든 전역적으로 사용하기 때문에 싱글톤 패턴을 사용해서 어디서든 접근 가능하게 만드는 것이 좋다.


import android.content.Context;
import android.content.SharedPreferences;

public class PrefsHelper {
    public static final String PREFERENCE_NAME="pref";
    private Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor prefsEditor;
    private static PrefsHelper instance;

    public static synchronized PrefsHelper init(Context context){
        if(instance == null)
            instance = new PrefsHelper(context);
        return instance;
    }

    private PrefsHelper(Context context) {
        mContext = context;
        prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE );
        prefsEditor = prefs.edit();
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        prefsEditor.putString(key, value);
        prefsEditor.commit();
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue);
    }

    public static void write(String key, Integer value) {
        prefsEditor.putInt(key, value).commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }
}


사용법은

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_intro);
        mContext = Intro.this;
        PrefsHelper.init(getApplicationContext()); // 한번만 실행하면 된다.
        // 네트워크 연결 검사
        if(Utils.NetworkConnection(mContext) == false) Utils.NotConnected_showAlert(mContext,this);
        checkPermissions();
    }


// 자동 로그인 체크 검사 (읽기)
userID = PrefsHelper.read("userid","");
userPW = PrefsHelper.read("userpw","");


// 쓰기

PrefsHelper.write("userid",userID);

PrefsHelper.write("userpw",userPW);



참고하면 좋은 자료

https://link2me.tistory.com/1528  Java Singleton Design Pattern


https://link2me.tistory.com/1821  Kotlin Singleton 코드 예제


https://link2me.tistory.com/1033  Android Preference

블로그 이미지

Link2Me

,
728x90

화면 전환을 한 경우, 로그인 화면은 다시 보이지 않도록 해야 한다.

 

val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK)
// FLAG_ACTIVITY_CLEARTOP : 실행할 activity가 이미 스택에 존재하면 해당 activity 위에 존재하는 다른 activity 모두 종료시킨다.
// FLAG_ACTIVITY_SINGLE_TOP : 호출하는 activity가 자신을 가리키는 경우, 기존의 activity를 재활용한다.
// 활동의 인스턴스가 이미 현재 작업의 맨 위에 있으면 시스템은 활동의 새 인스턴스를 생성하지 않고
// onNewIntent() 메서드를 호출하여 인텐트를 기존 인스턴스로 라우팅한다.
// https://developer.android.com/guide/components/activities/tasks-and-back-stack 읽어보면 된다.
// FLAG_ACTIVITY_CLEAR_TASK :
// FLAG_ACTIVITY_NEW_TASK : service, notification 등에서 액티비티를 시작하기 위해선 꼭 붙여야 한다.
// 스택에 없으면 새로운 task 생성하고 launch 시키고, 스택에 있으면 해당 task가 Foreground로 온다.
startActivity(intent)
finish() // 현재 activity 제거. login 화면은 다시 사용하지 않도록 하기 위해서
 

 

 

 

 

메모를 추가/수정하고, AddEditMemoActivity 를 종료하면서, 아래와 같은 FLAG를 지정한 경우

val intent = Intent(this@AddEditMemoActivity, MainActivity::class.java)
intent.apply {
    this.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)
overridePendingTransition(R.anim.fadein, R.anim.fadeout)
finish()
 

 

MainActivity 는 onNewIntent 를 수행하므로 여기에 필요한 코드를 추가한다.

 
override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    Log.d(TAG, "onNewIntent()")
    getMemoFromServer()
}
블로그 이미지

Link2Me

,
728x90

Java 기반 volley 라이브러리에 대한 사용예제는 https://link2me.tistory.com/1533 를 참조하면 된다.


앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.volley"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

    }

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

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
}


AndroidManifest.xml

- 필요한 퍼미션을 추가한다.

- android:usesCleartextTraffic="true" 를 추가한다. Android 9 이상에서 http 통신 가능하도록 하기 위해서.

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.READ_CALL_LOG" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <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.NoActionBar"
        android:usesCleartextTraffic="true">
        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>
 


activity_login.xml

- https://link2me.tistory.com/1807 참조


LoginActivity.kt

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.telephony.PhoneNumberUtils
import android.telephony.TelephonyManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlinx.android.synthetic.main.activity_login.*
import org.json.JSONException
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.HashMap

class LoginActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        context = this@LoginActivity
        initView()
    }

    fun initView() {
        // 코틀린에서는 더 이상 findViewById()를 사용하지 않아도 된다.
        // Kotlin Android Extension 만 적용시키면, 레이아웃 import 시키는 것만으로도 XML에서 정의내린 위젯들을 Id로 읽어들인다.
        btn_login.setOnClickListener{
            val url: String = "http://www.abc.com/mobile/mlogin.php"
            val loginID: String = et_id.text.toString().trim()
            val loginPW: String = et_pw.text.toString().trim()
            loginVolley(this, url, loginID, loginPW)
        }
    }

    private fun loginVolley(context: Context, url: String, userid: String, password: String) {
        // https://developer.android.com/training/volley/simple GET 방법

        // 1. RequestQueue 생성 및 초기화
        val requestQueue = Volley.newRequestQueue(context)

        // 2. Request Obejct인 StringRequest 생성
        val request: StringRequest = object : StringRequest(Method.POST, url,
            Response.Listener { response ->
                showJSONList(response)
            },
            Response.ErrorListener { error ->
                Toast.makeText(context, error.toString(), Toast.LENGTH_LONG).show()
            }
        ) {
            override fun getParams(): Map<String, String> {
                val params: MutableMap<String,String> = HashMap()
                params["userid"] = userid
                params["password"] = password
                params["mobileNO"] = getPhoneNumber() // 로그인하는 휴대폰번호 정보
                params["uID"] = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
                return params
            }
        }
        // 3) 생성한 StringRequest를 RequestQueue에 추가
        requestQueue.add(request)
    }

    fun showJSONList(response: String) {
        try {
            Log.e("TAG",response)
            val jsonObject = JSONObject(response)
            jsonObject.let {
                if(it.getString("userinfo") == null) {
                    showAlert(it.getString("result").toString(), it.getString("message").toString())
                } else {
                    if(it.getString("result").toString().equals("success")){
                        val jsonInfo = JSONObject(it.getString("userinfo").toString())
                        jsonInfo.let{
                            // Preference 에 대한 정보 기록은 생략한다.
                            startActivity(Intent(this,MainActivity::class.java))
                        }
                    }
                }
            }
        } catch (e: JSONException) {
            e.printStackTrace()
        }
    }

    companion object {
        var context: Context? = null

        @SuppressLint("MissingPermission") // TED퍼미션을 미리 설정했다는 가정하에
        fun getPhoneNumber(): String {
            var phoneNumber = ""
            try {
                val telephony = context?.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
                if (telephony.line1Number != null) {
                    phoneNumber = telephony.line1Number
                } else {
                    if (telephony.simSerialNumber != null) {
                        phoneNumber = telephony.simSerialNumber
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
            if (phoneNumber.startsWith("+82")) {
                phoneNumber = phoneNumber.replace("+82", "0")
            }
            phoneNumber = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                PhoneNumberUtils.formatNumber(
                    phoneNumber,
                    Locale.getDefault().country
                )
            } else {
                PhoneNumberUtils.formatNumber(phoneNumber)
            }
            return phoneNumber
        }

        fun showAlert(title: String, message: String) {
            val builder = context?.let {
                AlertDialog.Builder(it)
                    .setTitle(title)
                    .setMessage(message)
                    .setCancelable(false)
                    .setPositiveButton("OK") { dialog, id -> dialog.dismiss() }
            }
            val alert = builder!!.create()
            alert.show()
        }
    }

}


블로그 이미지

Link2Me

,
728x90

Java 코드로 된 Tedpermission 을 코틀린에서 사용하기 위한 방법이다.

자바로 된 코드를 코틀린 파일에 붙여넣기 하면 대부분 기초적인 변환이 이루어진다.

이후 약간의 손을 보면 사용이 가능해진다.

 

앱 build.gradle

 

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.tedpermission"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

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

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'gun0912.ted:tedpermission:2.0.0'
}

2021.7월 확인사항

implementation 'io.github.ParkSangGwon:tedpermission:2.3.0' 로 TED Permission 이 변경되었더라.

 

 

AndroidManifest.xml

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.READ_CALL_LOG" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <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=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>


LoginActivity.kt

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.TedPermission

class LoginActivity : AppCompatActivity() {
    var context: Context? = null

    var permissionlistener: PermissionListener = object : PermissionListener {
        override fun onPermissionGranted() { // 권한 허가시 실행 할 내용
            initView()
        }

        override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
            // 권한 거부시 실행  할 내용
            Toast.makeText(this@LoginActivity, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT)
                .show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        context = this@LoginActivity;
        checkPermissions()
    }

    private fun checkPermissions() {
        if (Build.VERSION.SDK_INT >= 26) { // 출처를 알 수 없는 앱 설정 화면 띄우기
            val pm: PackageManager = context!!.getPackageManager()
            Log.e("Package Name", packageName)
            if (!pm.canRequestPackageInstalls()) {
                startActivity(
                    Intent(
                        Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
                        Uri.parse("package:$packageName")
                    )
                )
            }
        }

        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                .setPermissionListener(permissionlistener)
                .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                .setPermissions(
                        Manifest.permission.READ_PHONE_STATE,
                        Manifest.permission.READ_CALL_LOG,  // 안드로이드 9.0 에서는 이것도 추가하라고 되어 있음.
                        Manifest.permission.CALL_PHONE,  // 전화걸기 및 관리
                        Manifest.permission.ACCESS_FINE_LOCATION,
                        Manifest.permission.ACCESS_COARSE_LOCATION
                ).check()
        } else {
            initView()
        }
    }

    fun initView() {

    }
}

 

'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] Activity 전환 Intent  (0) 2020.05.09
[코틀린] volley 라이브러리 사용예제  (0) 2020.05.09
[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] ViewPager 만들기  (0) 2020.04.24
[코틀린] webView 예제1  (0) 2020.04.20
블로그 이미지

Link2Me

,
728x90

프로퍼티(Property)

Java에서는 데이터르 필드에 저장하며, 멤버 필드의 가시성은 보통 비공개(private)이다.

클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메소드를 제공한다.

보통은 필드를 읽기 위한 getter를 제공하고 필드를 변경 허용해야 할 경우 setter를 추가 제공할 수 있다.

Java에서는 필드와 접근자를 묶어 property 라구 부르며, 프로퍼티를 활용하는 프레임워크가 많다.

코틀린은 프로퍼티를 언어 기본 기능으로 제공한다.

class Person(val name: String, val age: Int)

코틀린에서는 Java 클래스의 getter를 val 프로퍼티처럼 사용할 수 있고, getter/setter 쌍이 있는 경우에는 var 프로퍼티처럼 사용할 수 있다.


코틀린에서는 하나의 파일에 하나 또는 여러개의 클래스를 정의할 수 있다.

하지만 유지보수의 용이성을 고려하여 가급적 하나의 클래스를 하나의 파일에 정의하는 것이 좋다.


가시성 제한자(visibility modifier)

 public

 함수나 속성이 클래스 외부에서 사용될 수 없다.

 코틀린의 기본 가시성은 public 이므로 가시성 제한자를 생략해도 된다.

 private

 함수나 속성이 정의된 클래스 내부에서만 사용될 수 있다.

 protected

 함수나 속성이 정의된 클래스 내부 또는 클래스의 서브 클래스에서만 사용될 수 있다.

 internal

 함수나 속성이 정의된 클래스가 포함된 모듈에서 사용될 수 있다.


코틀린에서는 자바에 없는 internal 가시성을 지원한다. 이것은 같은 모듈에 있는 클래스, 함수, 속성까리 상호 사용할 수 있다는 것을 뜻한다.


Java는 기본적으로 패키지 가시성을 지원한다. 즉, 가시성 제한자가 없는 메서드, 필드, 클래스는 같은 package에 있는 클래스에서만 사용 가능하다는 뜻이다.

코틀린에서는 package 가시성이 없다. 같은 package에 있는 클래스, 함수, 속성 등은 기본적으로 상호 사용할 수 있어서 굳이 별도의 가시성을 가질 필요가 없기 때문이다.


서로 다른 패키지에 있는 같은 이름의 클래스나 함수 등을 함께 사용할 경우에는 as 키워드로 별칭을 지정하여 이름 충돌이 생기지 않게 할 수 있다.


클래스 속성에서 var 는 변경 가능한 속성을, val 은 읽기 전용 속성을 지정하는 데 사용된다.


코틀린 생성자

코틀린은 자바, C++과는 달리 클래스 선언 부분에서 이름에 괄호를 붙여 생성자를 만들 수 있다.

이를 주 생성자(primary constructor)라고 한다.

코틀린 몸체에 보조 생성자가(secondary constructor)를 추가할 수 있다. 보조 생성자는 매개변수를 다르게 하여 여러 개를 정의할 수 있다.

    // (name: String, surname: String)  default constructor signature
    class Person(_name: String, surname: String) {
        // 맨 앞에 밑줄(_)이 있는 변수는 임시 변수를 나타낸다.
        // 매개변수를 포함해서 임시 변수는 한번 이상 참조될 필요가 없는 변수이며,
        // 1회용이라는 것을 나타내기 위해 이름 앞에 밑줄을 붙인다.

        val name: String

        // init block , represents the body of default constructor
        init { //주생성자와 함께 초기화 블럭
            // init 초기화 블록은 여러개 가능하다.
            name = _name
            Log.d("primary", name)
        }

        // secondary constructor
        // this(name,"") call to default constructor
        // constructor 로부터 생성된 생성자는 '기본 생성자'를 '상속' 받아야 한다.
        constructor(name : String):this(name,""){
            Log.d("secondary", "Hello")
        }
    }


class Person constructor(name: String) { /*...*/ }

primary constructor가 어노테이션이나 접근 제한자(public, private 등)을 갖고 있지 않다면 constructor 키워드를 생략할 수 있다.
class Person(name: String) { /*...*/ }

자바의 경우 생성자로부터 받아온 변수를 해당 클래스에서 사용할 때 생성자 내에서 [this.클래스변수 = 매개변수] 같은 형식으로 받아올 수 있다. 하지만 코틀린은 생성자 영역이 없어서 이렇게 할 수가 없다. 

코틀린에서는 기본과 보조 생성자에 추가하여 클래스 초기화 블럭도 정의할 수 있다.

초기화 블럭은 init 키워드로 정의한다. init 초기화 블록은 여러개 가능하다.

this.name = name

만일 속성을 초기화하는 코드가 복잡하다면 초기화 코드를 함수나 초기화 블럭에 넣은 것을 고려하자.


코틀린의 클래스는 secondary constructor도 가질 수 있다.
이때 사용되는 키워드는 constructor이고, primary constructor와 다르게 생략할 수 없다.
class Person {
    var children: MutableList<Person> = mutableListOf<Person>();
    constructor(parent: Person) {
        parent.children.add(this)
    }
}



블로그 이미지

Link2Me

,
728x90

null 은 var 나 val 변수의 값이 없다는 것을 나타내는 특별한 값이다. 자바를 포함해서 많은 프로그래밍 언어에서 null 은 흔히 크래시(crash)를 유발하는 원인이 된다.


코틀린은 컴파일 언어다. 즉 실행에 앞서 컴파일러라는 특별한 프로그램에 의해 기계어로 변환된다는 걸 뜻한다.

컴파일러는 null 가능 타입에 null 값이 지정되는지 검사한다.


런타임 에러는 컴파일러가 발견할 수 없어서 프로그램이 컴파일된 후 실행 중에 발생하는 오류다.

자바는 null 가능 타입과 불가능 타입을 구분하지 않으므로 자바 컴파일러는 null 값으로 인한 문제가 있는지 사전에 알려줄 수가 없다. 따라서 정상적으로 컴파일 된 코드가 런타임 시에 크래시될 수 있다.


null 가능 타입이 컴파일이나 런타임 에러 없이 항상 실행되도록 해야 할 경우가 있다.

이런 경우에 null 가능 타입을 안전하게 처리하기 위해 안전 호출 연산자(safe call operator)인 ?.을 사용한다.

non-null 단언 연산자(assertion operator)인 !!도 null 가능 타입에 대해 함수를 호출하기 위해 사용할 수 있다.

non-null 단언 연산자는 컴파일러가 null 발생을 미리 알 수 없는 상황이 생길 수 있을 때 사용된다.

nul 가능 변수에 !! 연산자를 사용하는 것은 위험하다.


예기치 않는 값으로 인해 우리가 의도치 않는 방향으로 프로그램이 실행될 수 있다. 따라서 코드 값을 작성할 때는 입력 값을 검사하는데 많은 노력을 기울여야 한다.


null 불가능 매개변수가 null 인자를 받지 않게 해주는 매커니즘에는 @NotNull 애노테이션이 있다.

@NotNull 애노테이션은 많은 자바 프로젝트 코드에서만 볼 수 있지만, 코틀린에서 자바 메서드를 호출할 때 특히 유용하다.


출처 : 빅너드랜치의 코틀린 프로그래밍에서 발췌 요약

http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9791188621538&orderClick=LAG&Kc=







블로그 이미지

Link2Me

,
728x90

SharedPreferences는 간단한 값을 저장할 때 주로 사용한다.
초기 설정 값이나 자동 로그인 여부 등 간단한 값을 저장할 때 DB를 사용하면 복잡하기 때문에 SharedPreferences를 사용하면 적합하다.
SharedPreferences는 어플리케이션에 파일 형태로 데이터를 저장한다. 데이터는 (key, value) 형태로 data/data/패키지명/shared_prefs 폴더 안에 xml 파일로 저장된다.
해당 파일은 어플리케이션이 삭제되기 전까지 보존된다.
SharedPreferences는 앱의 어디서든 전역적으로 사용하기 때문에 싱글톤 패턴을 사용해서 어디서든 접근 가능하게 만드는 것이 좋다.


아래 코드는 https://link2me.tistory.com/1819 에서 Java로 구현했던 코드를 약간 수정해서 자바 코드를 Kotlin 으로 자동 변환한 다음에 다시 companion object 로 옮기면서 테스트하고 적어둔다.


예제1)

import android.content.Context
import android.content.SharedPreferences

class PrefsHelper {
    companion object{
        // 물음표(?)는 해당 타입의 null 가능 버전을 나타낸다.
        private var prefs: SharedPreferences? = null
        private var editor: SharedPreferences.Editor? = null
        private var prefmodule: PrefsHelper? = null
        val PREF_NAME = "pref"

        val BaudRate = "BaudRate"
        val DataBit = "DataBit"
        val StopBit = "StopBit"
        val Parity = "Parity"
        val Settings = "0"

        fun getInstance(context: Context): PrefsHelper? {
            if (prefmodule == null) {
                prefmodule = PrefsHelper()
            }
            if (prefs == null) {
                prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
                editor = prefs!!.edit()
            }
            return prefmodule
        }
    }

    fun getString(key: String, defValue: String): String {
        // ?. 안전호출 연산자(safe call operator) → NullPointerException 방지
        // !!  non-null 단언 연산자
        return prefs?.getString(key, defValue)!!
    }

    fun putString(key: String, value: String) {
        // !!  non-null 단언 연산자
        prefs!!.edit().apply{
            putString(key, value)
        }.apply()
    }

    fun getBoolean(key: String, defValue: Boolean): Boolean {
        return prefs?.getBoolean(key, defValue)!!
    }

    fun putBoolean(key: String, value: Boolean) {
        prefs!!.edit().apply{
            putBoolean(key, value)
        }.apply()
    }

    fun getInt(key: String, defValue: Int): Int {
        return prefs!!.getInt(key, defValue)
    }

    fun putInt(key: String, value: Int) {
        prefs!!.edit().apply{
            putInt(key, value)
        }.apply()
    }
}


MainActivity.kt 에서 쓰기와 읽기 테스트 한 예제

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        writePreferences("115200", "8", "1", 0)

        val BaudRate: String =  PrefsHelper.getInstance(this)!!.getString(PrefsHelper.BaudRate, "")
        val DataBit: String =  PrefsHelper.getInstance(this)!!.getString(PrefsHelper.DataBit, "")
        val StopBit: String =  PrefsHelper.getInstance(this)!!.getString(PrefsHelper.StopBit, "")
        val Parity: String = PrefsHelper.getInstance(this)!!.getString(PrefsHelper.Parity, "")

        Log.e("Baurate",BaudRate)
        Log.e("DataBit",DataBit)
        Log.e("StopBit",StopBit)
        Log.e("Parity",Parity)
    }

    private fun writePreferences(baudRate: String, dataBit: String, stopBit: String, parity: Int) {
        // !!.  non-null 단언 연산자
        PrefsHelper.getInstance(this)!!.putString(PrefsHelper.BaudRate, baudRate)
        PrefsHelper.getInstance(this)!!.putString(PrefsHelper.DataBit, dataBit)
        PrefsHelper.getInstance(this)!!.putString(PrefsHelper.StopBit, stopBit)
        PrefsHelper.getInstance(this)!!.putString(PrefsHelper.Parity, parity.toString())
        PrefsHelper.getInstance(this)!!.putString(PrefsHelper.Settings, "0")
    }
}


예제2)

import android.content.Context
import android.content.SharedPreferences

class PrefsHelper(context: Context) {

    companion object {
        const val PREFERENCE_NAME = "pref"
        private lateinit var prefs: SharedPreferences
        private lateinit var prefsEditor: SharedPreferences.Editor
        private var instance: PrefsHelper? = null

        fun init(context: Context): PrefsHelper? {
            if (instance == null) instance = PrefsHelper(context)
            return instance
        }

        fun read(key: String?, defValue: String?): String {
            return prefs.getString(key, defValue)!!
        }

        fun write(key: String?, value: String?) {
            prefsEditor.putString(key, value)
            prefsEditor.commit()
        }

        fun read(key: String?, defValue: Int): Int {
            return prefs.getInt(key, defValue)
        }

        fun write(key: String?, value: Int?) {
            prefsEditor.putInt(key, value!!).commit()
        }

        fun read(key: String?, defValue: Boolean): Boolean {
            return prefs.getBoolean(key, defValue)
        }

        fun write(key: String?, value: Boolean) {
            prefsEditor.putBoolean(key, value)
            prefsEditor.commit()
        }
    }

    init {
        prefs = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
        prefsEditor = prefs.edit()
    }

}

사용법

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_intro)
        mContext = this@Intro
        PrefsHelper.init(applicationContext) // 한번만 실행하면 된다.
    }


PrefsHelper.write("userid", userID)
PrefsHelper.write("userNM", Value.decrypt(result.userinfo!!.userNM))


블로그 이미지

Link2Me

,
728x90

안드로이드에서 Java Singleton 패턴으로 SharedPreferences 처리를 하는 코드를 적어둔다.


import android.content.Context;
import android.content.SharedPreferences;

public class PrefsHelper {
    private static Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor editor;
    private static PrefsHelper prefmodule = null;
    public static final String PREF_NAME ="pref";

    public static final String BaudRate = "BaudRate";
    public static final String DataBit = "DataBit";
    public static final String StopBit = "StopBit";
    public static final String Parity ="Parity";

    public static PrefsHelper getInstance(Context context) {
        mContext = context;

        if (prefmodule == null) {
            prefmodule = new PrefsHelper();
        }
        if(prefs==null){
            prefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
            editor = prefs.edit();
        }
        return prefmodule;
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putString(key, value);
        prefsEditor.commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue);
    }

    public static void write(String key, Integer value) {
        SharedPreferences.Editor prefsEditor = prefs.edit();
        prefsEditor.putInt(key, value).commit();
    }
}


사용법 write

private void writePreferences(String baudRate, String dataBit, String stopBit, int parity){
    PrefsHelper.getInstance(this).write(PrefsHelper.BaudRate, baudRate);
    PrefsHelper.getInstance(this).write(PrefsHelper.DataBit, dataBit);
    PrefsHelper.getInstance(this).write(PrefsHelper.StopBit, stopBit);
    PrefsHelper.getInstance(this).write(PrefsHelper.Parity, String.valueOf(parity));
}


읽기

String BaudRate = PrefsHelper.getInstance(mContext).read(PrefsHelper.BaudRate,null);
String DataBit = PrefsHelper.getInstance(mContext).read(PrefsHelper.DataBit,null);
String StopBit = PrefsHelper.getInstance(mContext).read(PrefsHelper.StopBit,null);
String Parity = PrefsHelper.getInstance(mContext).read(PrefsHelper.Parity,null);




다른 예제

이 예제를 활용하면 여러모로 편하다.

import android.content.Context;
import android.content.SharedPreferences;

public class PrefsHelper {
    public static final String PREFERENCE_NAME="pref";
    private Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor prefsEditor;
    private static PrefsHelper instance;

    public static synchronized PrefsHelper init(Context context){
        if(instance == null)
            instance = new PrefsHelper(context);
        return instance;
    }

    private PrefsHelper(Context context) {
        mContext = context;
        prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE );
        prefsEditor = prefs.edit();
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        prefsEditor.putString(key, value);
        prefsEditor.commit();
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue);
    }

    public static void write(String key, Integer value) {
        prefsEditor.putInt(key, value).commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }
}


사용방법

PrefsHelper.init(getApplicationContext()); // 한번만 실행하면 된다.


// 쓰기

PrefsHelper.write("userid",userID);
PrefsHelper.write("userpw",userPW);


// 읽기

userID = PrefsHelper.read("userid","");
userPW = PrefsHelper.read("userpw","");

블로그 이미지

Link2Me

,
728x90

Java 기반 ViewPager 와 동일한 Layout 으로 테스트하고 적어둔다.


앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.viewpger"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

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

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    //implementation "org.jetbrains.anko:anko:$anko_version"

    implementation 'com.google.android.material:material:1.1.0'
    //implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    //implementation 'androidx.recyclerview:recyclerview:1.1.0'

    // 이미지 출력용 Glide
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0' // Kotlin plugin doesn't pick up annotationProcessor dependencies

}



전체적인 코드는 아래 그림을 참조하면 알 수 있다.

MainActivity.kt, Fragment1.kt, Fragment2.kt, Fragment3.kt, ViewPagerAdapter.kt


먼저 activity_main.xml 을 선택하고 2번 검색버튼을 눌러서 tabLayout을 검색하면 Material Design 기반 tabLayout이 선택된다. Material Design 기반으로 하는 이유는 화면이 깔끔하기 때문이다.



Layout 코드는

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        app:tabTextColor="@color/colorDarkGray"
        app:tabSelectedTextColor="@color/colorWhite"
        app:layout_collapseMode="pin"
        app:tabGravity="fill"
        app:tabBackground="@drawable/tab_color_selector"
        app:layout_constraintBottom_toTopOf="@+id/viewPager"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Monday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Tuesday" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Wednesday" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="1dp"
        android:layout_marginEnd="1dp"
        android:layout_marginBottom="1dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tabLayout" />

</androidx.constraintlayout.widget.ConstraintLayout>


코틀린 코드

먼저 각각의 페이지를 담당하는 Fragment 를 만든다.

그런 다음 ViewPager 와 연결해준다.


class Fragment1 : Fragment() {

    // https://blog.mindorks.com/android-material-tabs-with-kotlin 참조해서 작성했다.

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_1, container, false)
        return view
    }
}

class Fragment2 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_2, container, false)
        return view
    }
}

class Fragment3 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_3, container, false)
        return view
    }
}


ViewPagerAdapter 코드 구현

class ViewPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager) {
    private val mFragmentList = ArrayList<Fragment>()
    private val mFragmentTitleList = ArrayList<String>()

    override fun getItem(position: Int): Fragment {
        return mFragmentList.get(position)
    }

    override fun getCount(): Int {
        return mFragmentList.size
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return mFragmentTitleList[position]
    }

    fun addFragment(fragment: Fragment, title: String) {
        mFragmentList.add(fragment)
        mFragmentTitleList.add(title)
    }
}


MainActivity.kt 코드

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val adapter = ViewPagerAdapter(supportFragmentManager)
        adapter.addFragment(Fragment1(), "사랑")
        adapter.addFragment(Fragment2(), "열정")
        adapter.addFragment(Fragment3(), "노력")
        viewPager.adapter = adapter
        tabLayout.setupWithViewPager(viewPager)
    }
}


테스트에 사용한 코드를 첨부

viewpger.zip

여기까지는 기본적인 ViewPager 동작에 대한 이해라고 보면 된다.


ViewPager 를 이용한 전자액자 예제는 https://github.com/junsuk5/kotlin-android 오준석의 생존코딩 코틀린편 자료 9장을 받아서 테스트해보면 된다. 최신으로 업데이트되어 있다.


Activity에서 Fragment 로 데이터를 보내려면 어떻게 해야 할까?

전자액자 예제를 기반으로 데이터 전달하는 걸 참조하는 예시라고 보면 된다.

class MainActivity : AppCompatActivity() {

    private lateinit var code: String

    lateinit var mWebViewFragment: Fragment1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        code = "1462"
        Log.e("MainActivity code",code)
        mWebViewFragment = Fragment1.newInstance(code)

        val adapter = ViewPagerAdapter(supportFragmentManager)
        adapter.addFragment(mWebViewFragment, "웹뷰")
        adapter.addFragment(Fragment2(), "열정")
        adapter.addFragment(Fragment3(), "노력")
        viewPager.adapter = adapter
        tabLayout.setupWithViewPager(viewPager)
    }
}

class Fragment1 : Fragment() {

    private lateinit var mWebView: WebView

    var code: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            code = it.getString(ARG_URI)
            Log.e("WebviewFragment code",code)
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        var view = inflater.inflate(R.layout.fragment_webview, container, false).apply {
            mWebView = findViewById(R.id.webView)
        }
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val url: String = "http://www.abc.com/getData.php?code=${code}"
        Log.e("onViewCreated code",url)
        url?.let { MyWebView(it) }
    }

    fun MyWebView(url: String){
        mWebView.apply {
            settings.javaScriptEnabled = true
            settings.setSupportZoom(true)
            settings.builtInZoomControls = true
            settings.displayZoomControls = false
            settings.loadsImagesAutomatically = true
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                settings.safeBrowsingEnabled = true  // api 26
            }
            settings.domStorageEnabled = true
            mWebView.webViewClient = WebViewClient()
        }
        mWebView.loadUrl(url)
    }

    companion object { // (1) 제일 먼저 호출됨
        // const키워드를 써서 전역변수 만들기
        private const val ARG_URI = "uri"

        @JvmStatic
        fun newInstance(uri: String) =
            Fragment1().apply {
                arguments = Bundle().apply {
                    putString(ARG_URI, uri)

                }
            }
    }

}




'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] Tedpermission 사용방법  (0) 2020.05.06
[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] webView 예제1  (0) 2020.04.20
[코틀린] RecyclerView Part 1  (0) 2020.04.19
Checking if a URL Exists in Kotlin  (0) 2020.04.17
블로그 이미지

Link2Me

,
728x90

코틀린에서 WebView 기능을 구글링해서 코드를 여러가지 추가하면서 테스트하고 적어둔다.

책에 나온 코드는 매우 간단하다.

사실 코드가 간단해도 동작하는데 전혀 문제는 없다.


Project build.gradle

buildscript {
    ext.kotlin_version = '1.3.72'
    ext.anko_version='0.10.8'

    repositories {
        google()
        jcenter()
       
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}


앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.webview"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

    }

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

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation "org.jetbrains.anko:anko:$anko_version"

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
}



import android.app.Activity
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.webkit.*
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var mWebView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mWebView = findViewById(R.id.webView)
        val url: String = "http://link2me.tistory.com/"
        MyWebView(url)

        urlEditText.setOnEditorActionListener{_, actionId, _->
            if(actionId == EditorInfo.IME_ACTION_SEARCH){
                var url: String = urlEditText.text.toString()
                if(!(url.contains("http://") || url.contains("https://"))){
                    url = "http://${url}"
                }
                MyWebView(url)
                true
            } else {
                false
            }
        }

    }

    fun MyWebView(url: String){
        mWebView.apply {
            settings.javaScriptEnabled = true
            // Enable and setup web view cache
            settings.setAppCacheEnabled(true)
            settings.cacheMode = WebSettings.LOAD_DEFAULT
            settings.setAppCachePath(cacheDir.path)
            // Enable zooming in web view
            settings.setSupportZoom(true)
            settings.builtInZoomControls = true
            settings.displayZoomControls = true
            // Enable disable images in web view
            settings.blockNetworkImage = false
            // Whether the WebView should load image resources
            settings.loadsImagesAutomatically = true

            settings.domStorageEnabled = true
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                settings.safeBrowsingEnabled = true  // api 26
            }
            //settings.pluginState = WebSettings.PluginState.ON
            settings.useWideViewPort = true
            settings.loadWithOverviewMode = true
            settings.javaScriptCanOpenWindowsAutomatically = true
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                settings.mediaPlaybackRequiresUserGesture = false
            }

            // More optional settings, you can enable it by yourself
            settings.domStorageEnabled = true
            settings.setSupportMultipleWindows(true)
            settings.loadWithOverviewMode = true
            settings.allowContentAccess = true
            settings.setGeolocationEnabled(true)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                settings.allowUniversalAccessFromFileURLs = true
            }

            settings.allowFileAccess = true

            // WebView settings
            mWebView.fitsSystemWindows = true

            mWebView.webViewClient = MyWebViewClient(this@MainActivity)
        }
        mWebView.loadUrl(url)
    }

    class MyWebViewClient internal constructor(private val activity: Activity) : WebViewClient() {

        @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
            val url: String = request?.url.toString();
            view?.loadUrl(url)
            return true
        }

        override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
            webView.loadUrl(url)
            return true
        }

        override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
            Toast.makeText(activity, "Got Error! $error", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
            mWebView.goBack()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }

    override fun onBackPressed() {
        if(mWebView.canGoBack()){
            mWebView.goBack() // 이전 페이지로 갈 수 있다면 이동하고
        } else {
            super.onBackPressed() // 더 이상 이전 페이지가 없을 때 앱이 종료된다.
        }
    }
}


XML Layout 만드는 방법은 별도 적지 않고 테스트한 소스코드를 첨부한다.

WebView.zip


'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] ViewPager 만들기  (0) 2020.04.24
[코틀린] RecyclerView Part 1  (0) 2020.04.19
Checking if a URL Exists in Kotlin  (0) 2020.04.17
Splash Screen with Kotlin  (0) 2020.04.09
블로그 이미지

Link2Me

,
728x90

Anko 라이브러리에 대한 사항은 https://github.com/Kotlin/anko 를 참조한다.


Anko Commons은 안드로이드 애플리케이션을 작성할 때 일반적으로 자주 구현하는 기능을 간편하게 추가할 수 있는 유틸리티 함수를 제공한다.

- AnKo Commons : 인텐트, 다이얼로그, 로그 등을 편리하게 사용하는 라이브러리
- AnKo Layouts : 안드로이드 레이아웃을 코드로 쉽게 작성하는 라이브러리
- AnKo SQLite : SQLite를 쉽게 사용하는 라이브러리
- AnKo Coroutines : 코루틴을 쉽게 사용하는 라이브러리


Project build.grade 추가사항

buildscript {
    ext.kotlin_version = '1.3.72'
    ext.anko_version = '0.10.8'

    repositories {
        google()
        jcenter()
       
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
} 


앱 build.gradle 추가사항

한번에 Anko 가 제공하는 모든 기능을 포함하고자 한다면 (including Commons, Layouts, SQLite)

dependencies {
    implementation "org.jetbrains.anko:anko:$anko_version"
}
 


dependencies {
    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:$anko_version"
}

블로그 이미지

Link2Me

,
728x90
앱 build.gradle Implementation 추가
RecyclerView는 기본 API에 제공되어 있지 않기 때문에, Support Library 추가를 해야 사용할 수 있다.

추가 방법의 한가지이다.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'


android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.recyclerview"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

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

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

    // 이미지 출력용 Glide
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

    kapt 'com.github.bumptech.glide:compiler:4.11.0'
    // Kotlin plugin doesn't pick up annotationProcessor dependencies

}
 


데이터 클래스 정의

data class PersonModel (
    var idx: String,
    var name: String,
    var position : String,
    var mobileNO: String,
    var checkBoxState: Boolean
)


Layout 만들기

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/item_listview" />

</androidx.constraintlayout.widget.ConstraintLayout>
 


Item View 생성

cardView 만드는 방법은 https://link2me.tistory.com/1813 참조


Adapter 구현

import android.content.Context
import android.os.AsyncTask
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.link2me.android.recyclerview.model.PersonModel
import kotlinx.android.synthetic.main.item_listview.view.*
import java.net.HttpURLConnection
import java.net.URL

class PersonAdapter(val list: List<PersonModel>, val context: Context) : RecyclerView.Adapter<PersonAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // XML객체를 실제 View 로 만들어주기 위한 작업
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_listview,parent,false)
        return ViewHolder(view)
    }

    override fun getItemCount(): Int {
        return list.count()
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 생성된 ViewHolder가 화면에 표시될 때 실제 데이터를 바인딩 해주는 함수이다.
        // ?. 연산자를 사용하면 null 값이 아닌 경우에만 호출된다.
        //holder?.bindItems(list[position], context) // 이렇게 하거나 아래줄과 같이 사용
        (holder as ViewHolder).bindItems(list[position],context)
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // 코틀린에서는 기본적으로 null 값을 허용하지 않는다.

        // null 값을 허용하려면 자료형의 오른쪽에 ? 기호를 붙여준다.
        // 변수 뒤에 !!를 추가하면 null 값이 아님을 보증하게 된다.
        // ?. 연산자를 사용하면 null 값이 아닌 경우에만 호출된다.
        val mImage = itemView?.findViewById(R.id.listitem_image) as ImageView

        fun bindItems (person: PersonModel, context: Context) {
            val imageUri: String = "http://www.abc.com/photos/${person.idx}.jpg"
            if (mImage != null) {
                val urlexist: Boolean = URLExistTask().execute(imageUri).get()
                if (urlexist) {
                    Glide.with(context).load(imageUri).override(126, 160).into(mImage)
                } else {
                    val resourceId: Int = R.drawable.photo_base
                    Glide.with(context).load(resourceId).override(126, 160).into(mImage)
                }
            };
            itemView.listitem_title.text = person.name
            itemView.listitem_subtext1.text = person.position
            itemView.listitem_subtext2.text = person.mobileNO
            itemView.listitem_checkbox.isChecked = person.checkBoxState

            itemView.setOnClickListener({
                Toast.makeText(context,person.name,Toast.LENGTH_SHORT).show()
            })

        }

    }

    inner class URLExistTask : AsyncTask<String, Void, Boolean>() {
        override fun doInBackground(vararg params: String?): Boolean {
            return try {
                HttpURLConnection.setFollowRedirects(false)
                val con: HttpURLConnection = URL(params[0]).openConnection() as HttpURLConnection
                con.setConnectTimeout(1000)
                con.setReadTimeout(1000)
                con.setRequestMethod("HEAD")
                con.getResponseCode() === HttpURLConnection.HTTP_OK
            } catch (e: Exception) {
                e.printStackTrace()
                false
            }
        }
    }

}

서버에 실제 이미지가 존재하는지 여부를 확인하기 위한 URLExistTask 메소드를 구현한다.


Adapter 생성

class MainActivity : AppCompatActivity() {

    var adapter: PersonAdapter?= null
    var personList: List<PersonModel> = ArrayList()
    // Java 에서는 new 키워드로 객체를 생성하지만, 코틀린에서는 new 키워드를 사용하지 않는다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        personList = listOf(
            PersonModel("250","홍길동","차장","010-111-1000",true),
            PersonModel("340","강감찬","부장","010-111-1001",true),
            PersonModel("419","이순신","본부장","010-111-1002",true),
            PersonModel("490","김구","부장","010-111-1003",true),
            PersonModel("503","양만춘","대리","010-111-1004",true),
            PersonModel("3","김두환","과장","010-111-1005",true),
            PersonModel("1","정성룡","사원","010-111-1006",true)
        )

        adapter = PersonAdapter(personList,this)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.setHasFixedSize(true)
        recyclerView.adapter = adapter
    }
}
 


리스트는 서버에서 가져온 리스트라고 가정하고 Local 데이터를 생성하여 테스트한다.


테스트에 사용한 소스코드를 첨부한다.

recyclerview_kotlin.zip


참고하면 좋은 자료

https://www.andreasjakl.com/kotlin-recyclerview-for-high-performance-lists-in-android/


Adapter 구현 수정사항

class PersonAdapter(val items: List<PersonModel>, val context: Context) : RecyclerView.Adapter<PersonAdapter.ViewHolder>() {

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 생성된 ViewHolder가 화면에 표시될 때 실제 데이터를 바인딩 해주는 함수이다.
        //(holder as ViewHolder).bindItems(list[position],context)
        var item = items[position]
        val listener = View.OnClickListener {it ->
            Toast.makeText(it.context, "Clicked: ${item.name}", Toast.LENGTH_SHORT).show()
        }
        holder.apply {
            // apply()함수는 블록에 객체 자신이 리시버 객체로 전달되고 이 객체가 반환된다.
            // 객체의 상태를 변화시키고 그 객체를 다시 반환할 때 주로 사용한다.
            bindItems(listener, item, context)
            itemView.tag = item
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // XML객체를 실제 View 로 만들어주기 위한 작업
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_listview,parent,false)
        return ViewHolder(view)
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // 코틀린에서는 기본적으로 null 값을 허용하지 않는다.

        // null 값을 허용하려면 자료형의 오른쪽에 ? 기호를 붙여준다.
        // 변수 뒤에 !!를 추가하면 null 값이 아님을 보증하게 된다.
        // ?. 연산자를 사용하면 null 값이 아닌 경우에만 호출된다.
        val mImage = itemView?.findViewById(R.id.listitem_image) as ImageView

        fun bindItems (listener: View.OnClickListener,person: PersonModel, context: Context) {
            val imageUri: String = "http://www.abc.com/photos/${person.idx}.jpg"
            if (mImage != null) {
                val urlexist: Boolean = URLExistTask().execute(imageUri).get()
                if (urlexist) {
                    Glide.with(context).load(imageUri).override(126, 160).into(mImage)
                } else {
                    val resourceId: Int = R.drawable.photo_base
                    Glide.with(context).load(resourceId).override(126, 160).into(mImage)
                }
            };
            itemView.listitem_title.text = person.name
            itemView.listitem_subtext1.text = person.position
            itemView.listitem_subtext2.text = person.mobileNO
            itemView.listitem_checkbox.isChecked = person.checkBoxState
            itemView.setOnClickListener(listener)
        }

    }

    inner class URLExistTask : AsyncTask<String, Void, Boolean>() {
        override fun doInBackground(vararg params: String?): Boolean {
            return try {
                HttpURLConnection.setFollowRedirects(false)
                val con: HttpURLConnection = URL(params[0]).openConnection() as HttpURLConnection
                con.setConnectTimeout(1000)
                con.setReadTimeout(1000)
                con.setRequestMethod("HEAD")
                con.getResponseCode() === HttpURLConnection.HTTP_OK
            } catch (e: Exception) {
                e.printStackTrace()
                false
            }
        }
    }

}
 


'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] ViewPager 만들기  (0) 2020.04.24
[코틀린] webView 예제1  (0) 2020.04.20
Checking if a URL Exists in Kotlin  (0) 2020.04.17
Splash Screen with Kotlin  (0) 2020.04.09
블로그 이미지

Link2Me

,
728x90

서버에서 사진 정보를 가져오는데 사진 파일이 존재하는지 URL 로 확인하는 코드이다.

RecyclerView 에 사진을 보여주는 cardView Item 작성할 때 적용했다.

메인쓰레드에서 처리하면 에러가 발생하므로 AsyncTask 를 이용한다.



앱 build.gradle

// 이미지 출력용 Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

    inner class URLExistTask : AsyncTask<String, Void, Boolean>() {
        override fun doInBackground(vararg params: String?): Boolean {
            return try {
                HttpURLConnection.setFollowRedirects(false)
                val con: HttpURLConnection = URL(params[0]).openConnection() as HttpURLConnection
                con.setConnectTimeout(1000)
                con.setReadTimeout(1000)
                con.setRequestMethod("HEAD")
                con.getResponseCode() === HttpURLConnection.HTTP_OK
            } catch (e: Exception) {
                e.printStackTrace()
                false
            }
        }
    }
 


사용법

val mImage: ImageView? = itemView?.findViewById(R.id.listitem_image)

val imageUri: String = "http://www.abc.com/photos/${person.idx}.jpg"

if (mImage != null) {
    val urlexist: Boolean = URLExistTask().execute(imageUri).get()
    if (urlexist) {
        Glide.with(context).load(imageUri).override(126, 160).into(mImage)
    } else {
        val resourceId: Int = R.drawable.photo_base
        Glide.with(context).load(resourceId).override(126, 160).into(mImage)
    }
};


#### 함수를 만드는 과정 ####

먼저 Java 로 된 함수를 찾는다.

public static boolean exists(String URLName) {
    try {
        HttpURLConnection.setFollowRedirects(false);
        HttpURLConnection con = (HttpURLConnection) new URL(URLName).openConnection();
        con.setConnectTimeout(1000);
        con.setReadTimeout(1000);
        con.setRequestMethod("HEAD");
        return (con.getResponseCode() == HttpURLConnection.HTTP_OK);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}


그런데 메인쓰레드에서 처리하면 android.os.NetworkOnMainThreadException kotlin 에러가 발생하므로 AsyncTask 에서 동작하도록 코드를 수정해야 한다.

'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] ViewPager 만들기  (0) 2020.04.24
[코틀린] webView 예제1  (0) 2020.04.20
[코틀린] RecyclerView Part 1  (0) 2020.04.19
Splash Screen with Kotlin  (0) 2020.04.09
블로그 이미지

Link2Me

,
728x90

앱 build.gradle 추가사항

implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.1.0'


ConstraintLayout 을 이용하여 Layout 구성





<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    app:cardCornerRadius="1dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/listitem_image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="5dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/listitem_title"
            app:srcCompat="@drawable/photo_base" />

        <TextView
            android:id="@+id/listitem_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="성명"
            android:textSize="20dp"
            android:layout_marginLeft="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/listitem_subtitles"
            app:layout_constraintStart_toEndOf="@+id/listitem_image"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:id="@+id/listitem_subtitles"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/listitem_checkbox"
            app:layout_constraintStart_toEndOf="@+id/listitem_title"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/listcell_subtext1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="서브타이틀1"
                android:textColor="@color/colorBlack"
                android:textSize="14dp"
                android:textStyle="normal" />

            <TextView
                android:id="@+id/listcell_subtext2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="서브타이틀2"
                android:textColor="@color/colorBlack"
                android:textSize="14dp"
                android:textStyle="normal" />

        </LinearLayout>

        <CheckBox
            android:id="@+id/listitem_checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:focusable="false"
            android:visibility="gone"
            android:layout_marginLeft="20dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/listitem_subtitles" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/item_listview" />

</androidx.constraintlayout.widget.ConstraintLayout>
 


'안드로이드 > Layout' 카테고리의 다른 글

EditText DatePicker  (0) 2020.08.18
Floating Action Button  (0) 2020.07.11
Meterial Design 로그인 Layout 예제  (0) 2020.03.21
ScrollView  (0) 2019.12.20
Dynamic Layouts  (0) 2019.01.06
블로그 이미지

Link2Me

,
728x90

온라인 코틀린 강좌 들으면서 디자인을 고려하여 Material Design 구글 검색 참조 및 코드 보완 테스트 하고 있다.

구글 검색어 : android kotlin splash

검색어로 찾을 결과 https://levelup.gitconnected.com/a-tutorial-on-building-a-splash-screen-with-kotlin-in-android-studio-dc647cd52f9b 를 참조했고, UI 관련 파일 내용은 약간 다르다.


앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.splash"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

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

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.0.0'
}
 


AndroidManifest.xml

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

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

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MainActivity" android:theme="@style/AppTheme.DayNight"/>

    </application>

</manifest>
 


color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#00A99D</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorPrimaryDarker">#CC1D1D</color>
    <color name="colorAccent">#D81B60</color>

    <color name="colorBlack">#ff000000</color>
    <color name="colorWhite">#ffffffff</color>

    <color name="colorGray">#ff8a8a8a</color>
    <color name="colorDarkGray">#4c4c4e</color>
    <color name="colorLightGray">#ffeaeaea</color>

    <color name="colorMint">#ff00c0aa</color>
    <color name="colorLightMint">#1000c0aa</color>

    <color name="colorJet">#222222</color>
    <color name="colorOil">#333333</color>
    <color name="colorMonsoon">#777777</color>
    <color name="colorJumbo">#888888</color>
    <color name="colorAluminum">#999999</color>
    <color name="colorBase">#AAAAAA</color>
    <color name="colorIron">#CCCCCC</color>
</resources>
 


styles.xml

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- This style is for the splash screen -->
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="AppTheme.DayNight" parent="Theme.AppCompat.DayNight.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>

        <item name="colorControlNormal">@color/colorIron</item>
        <item name="colorControlActivated">@color/colorWhite</item>
        <item name="colorControlHighlight">@color/colorWhite</item>
        <item name="android:textColorHint">@color/colorIron</item>

        <item name="colorButtonNormal">@color/colorPrimaryDarker</item>
        <item name="android:colorButtonNormal">@color/colorPrimaryDarker</item>
    </style>

    <style name="AppTheme.Dark.Dialog" parent="Theme.AppCompat.Dialog">
        <item name="colorAccent">@color/colorWhite</item>
        <item name="android:textColorPrimary">@color/colorIron</item>
        <item name="android:background">@color/colorPrimary</item>
    </style>
</resources>
 


activity_splash.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
    tools:context=".SplashActivity">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/bg_splash"/>

</androidx.constraintlayout.widget.ConstraintLayout>
 


SplahsAcitivity.kt

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity

class SplashActivity : AppCompatActivity() {
    private val SPLASH_TIME_OUT:Long = 2000 // 2 sec

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)

        Handler().postDelayed({
            startActivity(Intent(this,MainActivity::class.java))
            finish()  // close this activity
        }, SPLASH_TIME_OUT)
    }
}
 


위 예제코드 파일

slpah_src.zip



'안드로이드 > Kotlin 기능' 카테고리의 다른 글

[코틀린] PrefsHelper  (0) 2020.05.04
[코틀린] ViewPager 만들기  (0) 2020.04.24
[코틀린] webView 예제1  (0) 2020.04.20
[코틀린] RecyclerView Part 1  (0) 2020.04.19
Checking if a URL Exists in Kotlin  (0) 2020.04.17
블로그 이미지

Link2Me

,
728x90

안드로이드 버전이 6.0 이상인 경우 출처를 알 수 없는 앱인 경우 설치 허용 체크가 해제된 상태다.

이 경우 설정을 체크하는 화면으로 이동하는 코드를 테스트 하고 적어둔다.


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


implementation 'gun0912.ted:tedpermission:2.0.0'


출처를 알 수 없는 앱 설정 코드

if(Build.VERSION.SDK_INT >= 26){ // 출처를 알 수 없는 앱 설정 화면 띄우기
    PackageManager pm = LoginActivity.this.getPackageManager(); // 현재 설치하는 앱
    boolean isTrue = pm.canRequestPackageInstalls();
    if (!pm.canRequestPackageInstalls()){ // 신뢰할 수 있는 앱 체크로 선택되어 있지 않다면....
        startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName())));

    }
}

private void NonSecretApp_Setting() {
    if(Build.VERSION.SDK_INT >= 26)  { // 출처를 알 수 없는 앱 설정 화면 띄우기
        PackageManager pm = getPackageManager();
        Log.e("Package Name",pm.getInstalledPackages(0).get(0).packageName);
        Log.e("Package Name",getPackageName());
        if (!pm.canRequestPackageInstalls()) {
            AlertDialog.Builder b = new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Light_Dialog);
            b.setTitle("알림");
            b.setMessage("보안을 위해 스마트폰 환경설정의 '앱 설치 허용'을 설정해 주시기 바랍니다.설정화면으로 이동하시겠습니까?");
            b.setCancelable(false);
            b.setPositiveButton("설정하기", (dialog, which) -> {
                Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);
            });

            b.setNegativeButton("건너띄기", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {

                }
            });
            b.show();
        }
    }
}


한번 신뢰 설정을 하면 두번다시 물어보지는 않으므로 안심하고 사용하면 된다.


PermissionListener permissionlistener = new PermissionListener() {
    @Override
    public void onPermissionGranted() {
        initView();
    }

    @Override
    public void onPermissionDenied(ArrayList<String> deniedPermissions) {
        Toast.makeText(LoginActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    context = LoginActivity.this;
    checkPermissions();
}

private void checkPermissions() {


    if(Build.VERSION.SDK_INT >= 26){ // 출처를 알 수 없는 앱 설정 화면 띄우기
        PackageManager pm = context.getPackageManager();
        Log.e("Package Name",getPackageName());
        boolean isTrue = pm.canRequestPackageInstalls();
        if (!pm.canRequestPackageInstalls()){
            startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
                    Uri.parse("package:" + getPackageName())));
        }
    }


    if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
        TedPermission.with(context)
                .setPermissionListener(permissionlistener)
                .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                .setPermissions(new String[]{
                        android.Manifest.permission.READ_PHONE_STATE,
                        android.Manifest.permission.READ_CALL_LOG, // 안드로이드 9.0 에서는 이것도 추가하라네
                        android.Manifest.permission.CALL_PHONE,  // 전화걸기 및 관리
                        android.Manifest.permission.ACCESS_FINE_LOCATION,
                        android.Manifest.permission.ACCESS_COARSE_LOCATION
                })
                .check();

    } else {
        initView();
    }
}
 


블로그 이미지

Link2Me

,
728x90

Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
        at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:267)
        at androidx.room.RoomDatabase.query(RoomDatabase.java:323)
        at androidx.room.util.DBUtil.query(DBUtil.java:83)


RoomDatabase 를 사용하는 방법에 대해 <오준석의 생존코딩> Morden Android 유투브 강좌를 듣고 구글링을 해서 내용을 좀 더 알게된 걸 적어둔다.


Room은 구글에서 만든 공식 ORM(Object-relational mapping)이라고 할 수 있으며 여러가지 강력한 기능들을 지원하고 있다. Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 강력한 데이터베이스 액세스를 지원하는 추상화 계층을 SQLite에 제공한다.



보다 자세한 내용은 https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0 를 참조한다.

위 URL 마지막에 나오는 https://github.com/googlecodelabs/android-room-with-a-view 자료를 받아서 테스트 해본다.


Project build.gradle

buildscript {
    ext.kotlin_version = '1.3.71'
    repositories {
        google()
        jcenter()
       
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
       
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}



앱 build.gradle 추가 사항

Google I/O 2018에서는 AndroidX 패키지를 소개하였다. 하지만 아직 28.x까지는 기존 패키지를 사용할 수 있다.

dependencies {
    def lifecycle_version = "2.2.0"

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0'

    implementation 'androidx.room:room-runtime:2.2.5'
    annotationProcessor 'androidx.room:room-compiler:2.2.5'

    // https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko
    // Lifecycle components (ViewModel and LiveData)
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"
}


activitity_main.xml 파일 만들기

https://www.youtube.com/watch?v=pG6OkJ3rSjg 동영상을 보는게 좋다.



Todo.java

import androidx.room.Entity;
import androidx.room.PrimaryKey;


// @Entity 어노테이션이 지정된 클래스는 데이터베이스의 테이블을 정의하는데 사용된다.
@Entity
public class Todo {
    // 1. 엔티티 클래스 만들기
    // 먼저 데이터 구조를 정의할 엔티티를 클래스 형태로 만들어 준다.

    @PrimaryKey(autoGenerate = true)
    private int id;
    private String title;

    public Todo(String title) {
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Todo{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}


TodoDao.java

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

import java.util.List;

@Dao
public interface TodoDao {
    // 2. DAO 인터페이스 만들기

    @Query("SELECT * FROM Todo")
    LiveData<List<Todo>> getAll();

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    void insert(Todo todo);

    @Update
    void update(Todo todo);

    @Delete
    void delete(Todo todo);

    @Query("DELETE FROM Todo")
    void deleteAll();

}


AppDatabase.java

import android.content.Context;

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Database(entities = {Todo.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    // 3. 데이터베이스 클래스 만들기
    // https://webcoding.tistory.com/ 참조하면 좋은 자료 많다.

    public abstract TodoDao todoDao(); // Dao 인터페이스
    private static volatile AppDatabase INSTANCE;

    private static final int NUMBER_OF_THREADS = 4;
    static final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);

    // 인스턴스를 생성하여 반환
    public static AppDatabase getInstance(Context context){
        if(INSTANCE == null){
            synchronized (AppDatabase.class){
                if(INSTANCE == null){
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),AppDatabase.class,"Todo.db")
                            //.allowMainThreadQueries()
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}


MainViewModel.java

import android.app.Application;
import android.os.AsyncTask;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;

import java.util.List;

public class MainViewModel extends AndroidViewModel {
    private AppDatabase db;

    public MainViewModel(@NonNull Application application) {
        super(application);
        db = AppDatabase.getInstance(application); // 데이터베이스 접근 인스턴스 사용하기
    }

    public LiveData<List<Todo>> getAll(){
        return db.todoDao().getAll();
    }

    public void insert(Todo todo){
        new InsertAsyncTask(db.todoDao()).execute(todo);
    }

    private static class InsertAsyncTask extends AsyncTask<Todo,Void, Void> {
        private TodoDao mTodoDao;

        public InsertAsyncTask(TodoDao todoDao) {
            this.mTodoDao = todoDao;
        }

        @Override
        protected Void doInBackground(Todo... todos) {
            mTodoDao.insert(todos[0]);
            return null;
        }
    }
}


MainActivity.java

viewModel = ViewModelProviders.of(this).get(MainViewModel.class);

ViewModelProviders.of()로 초기화하던 방식이 2.2.0에서 deprecated 되었으므로 아래와 같이 수정한다.

import android.os.Bundle;
import android.widget.EditText;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelStore;
import androidx.lifecycle.ViewModelStoreOwner;

public class MainActivity extends AppCompatActivity implements ViewModelStoreOwner {
    private EditText mTodoEditText;
    private TextView mResultTextView;

    private ViewModelProvider.AndroidViewModelFactory viewModelFactory;
    private ViewModelStore viewModelStore = new ViewModelStore();
    private MainViewModel viewModel;

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

        mTodoEditText = findViewById(R.id.todo_edit);
        mResultTextView = findViewById(R.id.result_text);

        if(viewModelFactory == null){
            viewModelFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
        }
        viewModel = new ViewModelProvider(this,viewModelFactory).get(MainViewModel.class);

        // UI 갱신
        viewModel.getAll().observe(this, todos -> {
            mResultTextView.setText(todos.toString());
        });

        // 버튼 클릭시 DB에 insert
        findViewById(R.id.add_btn).setOnClickListener(v -> {
            viewModel.insert(new Todo(mTodoEditText.getText().toString()));
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        viewModelStore.clear();
    }

    @Override
    public ViewModelStore getViewModelStore(){
        return viewModelStore;
    }
}


<오준석의 생존코딩 모던 안드로이드> 유투브 강좌로 부족한 부분은 https://github.com/tazihad/Android---Room-ViewModel-LiveData-RecyclerView-MVVM---Example 에 나오는 자료를 받고, 관련 유투브 강좌 https://www.youtube.com/watch?v=ARpn-1FPNE4 를 보면 도움이 될 것이다.


유투브 강좌를 듣고 따라한 코딩 소스를 첨부한다.

notesqlite-1.zip


https://guides.codepath.com/android/Room-Guide 를 보면 Room 사용법에 대해 조금 더 이해하는데 도움된다.


notesqlite-2.zip


@Entity(tableName = "note_table", indices = @Index(value = {"title"}, unique = true))
public class Note {

    @PrimaryKey(autoGenerate = true)
    private int id;
    private String title; // 중복 입력 체크
    private String description;
    private int priority;

    public Note(String title, String description, int priority) {
        this.title = title;
        this.description = description;
        this.priority = priority;
    }
}

@Dao
public interface NoteDao {

    // 중복 값 입력시 새로운 값으로 변경됨.
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insert(Note note);

    // 기존에 존재하는 제목 값으로 변경시 무시
    @Update(onConflict = OnConflictStrategy.IGNORE)
    void update(Note note);

    @Delete
    void delete(Note note);

    @Query("DELETE FROM note_table")
    void deleteAllNotes();

    @Query("SELECT * FROM note_table ORDER BY priority DESC")
    LiveData<List<Note>> getAllNotes();

}


중복 입력 체크 예시

출처 : https://stackoverflow.com/questions/46916388/android-room-inserts-duplicate-entities


@Entity(tableName = "cameras", indices = [Index(value = ["accountId","dvrId"], unique = true)])
public class CameraEntity {
    @PrimaryKey(autoGenerate = true)
    private int id;
    private Integer accountId;
    private Integer dvrId;
    private String vendorId;
}

@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<CameraEntity> values);



@Dao
public interface UserDao {
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}


출처 : https://developer.android.com/training/data-storage/room#java
@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

블로그 이미지

Link2Me

,