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

,