728x90

viewModel 기반에서 RecyclerView 처리하는 예제를 작성해보고 있는데 생각만큼 쉽지 않다.

viewBinding 은 기본으로 사용해야 하는 것 같아 viewBinding 처리했고 viewModel 부분은 많은 연습을 해야 사용법이 익숙해질 듯 하다.

 

 
import com.link2me.android.recyclerviewmodel.adapter.ContactsListAdapter
import com.link2me.android.recyclerviewmodel.databinding.ActivityMainBinding
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.model.ContactDataDto
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitService
import com.link2me.android.recyclerviewmodel.viewmodel.MainViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
 
class MainActivity : AppCompatActivity() {
    private val TAG = this.javaClass.simpleName
 
    lateinit var mContext: Context
    private lateinit var mAdapter: ContactsListAdapter // 리스트뷰에 사용되는 ListViewAdapter
 
    private val addressItemList = mutableListOf<ContactData>() // 서버에서 가져온 원본 데이터 리스트
    private val searchItemList = mutableListOf<ContactData>() // 서버에서 가져온 원본 데이터 리스트
 
    private lateinit var backPressHandler: BackPressHandler
 
    val binding by lazy { ActivityMainBinding.inflate(layoutInflater)  }
 
    lateinit var viewModel: MainViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        //setContentView(R.layout.activity_main)
        mContext = this@MainActivity
 
        Log.e(TAG, "onCreate")
 
        backPressHandler = BackPressHandler(this// 뒤로 가기 버튼 이벤트
 
        // 뷰모델 가져오기
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
 
        // 관찰하여 데이터 값이 변경되면 호출
        viewModel.getContactsData().observe(this, listUpdateObserver)
    }
 
    var listUpdateObserver: Observer<List<ContactData>> =
        Observer {
            mAdapter = ContactsListAdapter(mContext) // Adapter 생성
            binding.myRecyclerView.adapter = mAdapter // 어댑터를 리스트뷰에 세팅
            binding.myRecyclerView.layoutManager = LinearLayoutManager(this)
            mAdapter.submitList(it)
 
            addressItemList.clear()
            addressItemList.addAll(it)
 
            binding.search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String): Boolean {
                    // 문자열 입력을 완료했을 때 문자열 반환
                    return false
                }
 
                override fun onQueryTextChange(newText: String): Boolean {
                    // 문자열이 변할 때마다 바로바로 문자열 반환
                    filter(newText)
                    return false
                }
            })
 
            mAdapter.setItemClickListener(object : ContactsListAdapter.OnItemClickListener {
                override fun onClick(v: View, position: Int) {
                    val contacts = addressItemList[position]
                    Toast.makeText(mContext, "클릭한 이름은 ${contacts.userNM} 입니다.", Toast.LENGTH_SHORT)
                        .show()
                }
            })
 
        }
 
    fun filter(charText: String) {
        // ListAdapter 에서 처리하는 방법을 알게되면 수정할 예정
        var charText = charText
        charText = charText.toLowerCase(Locale.getDefault())
        searchItemList.clear()
        if (charText.length == 0) {
            mAdapter.submitList(addressItemList)
        } else {
            for (wp in addressItemList) {
                if (Utils.isNumber(charText)) { // 숫자여부 체크
                    if (wp.mobileNO.contains(charText) || wp.officeNO.contains(charText)) {
                        // 휴대폰번호 또는 사무실번호에 숫자가 포함되어 있으면
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    }
                } else {
                    val iniName = HangulUtils.getHangulInitialSound(wp.userNM, charText)
                    if (iniName!!.indexOf(charText) >= 0) { // 초성검색어가 있으면 해당 데이터 리스트에 추가
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    } else if (wp.userNM.toLowerCase(Locale.getDefault()).contains(charText)) {
                        searchItemList.add(wp)
                        mAdapter.submitList(searchItemList)
                    }
                }
            }
        }
        mAdapter.notifyDataSetChanged()  // 이거 없애면 어플 비정상 종료 처리됨
    }
 
    override fun onBackPressed() {
        backPressHandler.onBackPressed()
    }
}

단순하게 데이터를 서버에서 받아서 RecyclerView 에 출력하는 것은 어렵지 않다.

검색 기능을 추가하려니까 기존 사용하던 방식의 RecyclerView 와 약간 다른 부분을 처리하기 위해 ArrayList 를 여러개 생성해서 하는 것이 비효율적인 듯 싶다.

 

package com.link2me.android.recyclerviewmodel.viewmodel
 
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.model.ContactDataDto
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
 
// 데이터의 변경사항을 알려주는 라이브 데이터를 가지는 뷰모델
class MainViewModel : ViewModel() {
    private val TAG = this.javaClass.simpleName
 
    // 뮤터블 라이브 데이터 - 수정 가능, 라이브 데이터 - 값 변경 안됨
    private val liveData: MutableLiveData<List<ContactData>> by lazy {
        MutableLiveData<List<ContactData>>().also {
            loadContacts()
        }
    }
 
    fun getContactsData(): LiveData<List<ContactData>> {
        return liveData
    }
 
    // LiveData는 관찰 가능한 데이터들의 홀더 클래스
 
    private fun loadContacts(){
        RetrofitService.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataDto> {
            override fun onResponse(call: Call<ContactDataDto>, response: Response<ContactDataDto>) {
                response.body()?.let {
                    if(it.status.contains("success")){
                        liveData.value = it.message
                    }
                }
            }
 
            override fun onFailure(call: Call<ContactDataDto>, t: Throwable) {
                Log.d(TAG,"Retrofit response fail.")
                t.stackTrace
            }
 
        })
    }
 
    fun searchNameChanged(name: String) {
 
    }
 
}
 
 

 

 

Adapter 코드는 포멧만 한번 만들어두면 몇가지만 코드만 수정하면 되기 때문에 재활용하는 것은 쉬운 편이다.

package com.link2me.android.recyclerviewmodel.adapter
 
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.telephony.PhoneNumberUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.link2me.android.recyclerviewmodel.databinding.ItemAddressBinding
import com.link2me.android.recyclerviewmodel.model.ContactData
import com.link2me.android.recyclerviewmodel.retrofit.RetrofitURL
import java.util.*
 
class ContactsListAdapter(val context: Context) : ListAdapter<ContactData, ContactsListAdapter.ViewHolder>(diffUtil) {
 
    // itemClickListener 를 위한 인터페이스 정의
    interface OnItemClickListener {
        fun onClick(v: View, position: Int)
    }
    private lateinit var itemClickListener: OnItemClickListener
 
    fun  setItemClickListener(itemClickListener: OnItemClickListener){
        this.itemClickListener = itemClickListener
    }
    //////////////////////////////////////////////////////////////////
 
    // val 예약어로 바인딩을 전달 받아서 전역으로 사용힌다. 그리고 상속받는 ViewHolder 생성자에는 꼭 binding.root를 전달해야 한다.
    inner class ViewHolder(private val binding: ItemAddressBinding) : RecyclerView.ViewHolder(binding.root){
        // View와 데이터를 연결시키는 함수
        fun bind(item: ContactData){
            //binding.executePendingBindings() //데이터가 수정되면 즉각 바인딩
 
            if (item.photo.contains("jpg")){
                val photoURL = RetrofitURL.Photo_URL + item.photo
                Glide.with(context).load(photoURL).into(binding.profileImage)
            }
 
            // 생성자에서 val로 받았기 때문에 홀더 내부 어디에서나 binding 사용가능
            binding.itemName.text = item.userNM
            binding.itemMobileNO.text = item.mobileNO
 
            val items = arrayOf("휴대폰 전화걸기""연락처 저장")
            val builder = AlertDialog.Builder(context)
            builder.setTitle("해당작업을 선택하세요")
            builder.setItems(items, DialogInterface.OnClickListener { dialog, which ->
                Toast.makeText(context, items[which] + "선택했습니다.", Toast.LENGTH_SHORT).show()
                when (which) {
                    0 -> {
                        if (item.mobileNO.length == 0) {
                            Toast.makeText(context, "전화걸 휴대폰 번호가 없습니다.", Toast.LENGTH_SHORT).show()
                            return@OnClickListener
                        }
                        val dialog1 = AlertDialog.Builder(context)
                            .setTitle(item.userNM)
                            .setMessage(PhoneNumberUtils.formatNumber(item.mobileNO) + " 통화하시겠습니까?")
                            .setPositiveButton("예",
                                { dialog, which ->
                                    val intent = Intent(Intent.ACTION_CALL,
                                        Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.mobileNO))
                                    )
                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    context.startActivity(intent)
                                })
                            .setNegativeButton("아니오") { dialog, which -> dialog.dismiss() }.create()
                        dialog1.show()
                    }
                    1 -> Toast.makeText(context, "전화번호 저장하는 로직은 직접 구현하시기 바랍니다.", Toast.LENGTH_SHORT).show()
                }
            })
            builder.create()
            binding.itemBtn.setOnClickListener { builder.show() }
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        //val rootView = LayoutInflater.from(parent.context).inflate(R.layout.item_address, parent,false)
        return ViewHolder(ItemAddressBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }
 
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출한다.
 
        holder.itemView.setOnClickListener {
            itemClickListener.onClick(it,position)
        }
 
        holder.apply {
            bind(currentList[position])
        }
    }
 
    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ContactData>() {
            override fun areContentsTheSame(oldItem: ContactData, newItem: ContactData) =
                oldItem == newItem
 
            override fun areItemsTheSame(oldItem: ContactData, newItem: ContactData) =
                oldItem.idx == newItem.idx
        }
    }
 
}
 

 

전체 소스코드는 https://github.com/jsk005/KotlinProjects/tree/master/recyclerviewmodel_1 에 올려두었다.

관련 기능을 하나 하나 테스트하면서 익히고 기록해 두려고 한다.

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

증감 추이를 파악하기 위해 샘플로 작성한 LineChart 예제이다.

 

앱 build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
}
 
android {
    compileSdkVersion 30
 
    defaultConfig {
        applicationId "com.link2me.android.mpchartdemo"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
 
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
 
dependencies {
 
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
 
    //implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
    implementation 'com.github.PhilJay:MPAndroidChart:v2.2.5'
}
 

 

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=".MainActivitySample">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        tools:ignore="MissingConstraints">
 
        <com.github.mikephil.charting.charts.LineChart
            android:id="@+id/linechart"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
 
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

MainActivity.kt 파일

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import com.github.mikephil.charting.utils.ColorTemplate
import java.util.*
 
class MainActivity : AppCompatActivity() {
    private val TAG = this.javaClass.simpleName
    lateinit var lineChart: LineChart
    private val chartData = ArrayList<ChartData>()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        chartData.clear()
        addChartItem("1월"7.9)
        addChartItem("2월"8.2)
        addChartItem("3월"8.3)
        addChartItem("4월"8.5)
        addChartItem("5월"7.3)
 
        LineChartGraph(chartData, "강남")
    }
 
    private fun addChartItem(lableitem: String, dataitem: Double) {
        val item = ChartData()
        item.lableData = lableitem
        item.valData = dataitem
        chartData.add(item)
    }
 
    private fun LineChartGraph(chartItem: ArrayList<ChartData>, displayname: String) {
        lineChart = findViewById(R.id.linechart)
 
        val entries = ArrayList<Entry>()
        for (i in chartItem.indices) {
            entries.add(Entry(chartItem[i].valData.toFloat(), i))
        }
 
        val depenses = LineDataSet(entries, displayname)
        depenses.axisDependency = YAxis.AxisDependency.LEFT
        depenses.valueTextSize = 12f // 값 폰트 지정하여 크게 보이게 하기
        depenses.setColors(ColorTemplate.COLORFUL_COLORS) //
        //depenses.setDrawCubic(true); //선 둥글게 만들기
        depenses.setDrawFilled(false//그래프 밑부분 색칠
 
        val labels = ArrayList<String>()
        for (i in chartItem.indices) {
            labels.add(chartItem[i].lableData)
        }
 
        val dataSets = ArrayList<ILineDataSet>()
        dataSets.add(depenses as ILineDataSet)
        val data = LineData(labels, dataSets) // 라이브러리 v3.x 사용하면 에러 발생함
 
        lineChart.data = data
        //lineChart.animateXY(1000,1000);
        lineChart.invalidate()
    }
}

 

data class ChartData(
    var lableData: String = "",
    var valData: Double = 0.0
)

 

 

728x90
블로그 이미지

Link2Me

,
728x90

GitHUB 에 공개된 샘플 예제는 Java 버전으로 되어 있다.

코틀린 버전 예제로 변경 연습한 코드를 적어둔다.

 

앱 build.gradle

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
}
 
android {
    compileSdkVersion 30
 
    defaultConfig {
        applicationId "com.kt.android.mpchart"
        minSdkVersion 26
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
 
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
 
dependencies {
 
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
 
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

 

 

 

activity_mail.xml

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        tools:ignore="MissingConstraints">
 
        <com.github.mikephil.charting.charts.LineChart
            android:id="@+id/linechart"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
 
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

코틀린 코드

MainActivity.kt

서버에서 데이터를 가져왔다고 가정하고 직접 입력된 값을 표기했다.

 

import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import java.util.*
 
class MainActivity : AppCompatActivity() {
    private val TAG = this.javaClass.simpleName
    lateinit var lineChart: LineChart
    private val chartData = ArrayList<ChartData>()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        // 서버에서 데이터 가져오기 (서버에서 가져온 데이터로 가정하고 직접 추가)
        chartData.clear()
        addChartItem("1월"7.9)
        addChartItem("2월"8.2)
        addChartItem("3월"8.3)
        addChartItem("4월"8.5)
        addChartItem("5월"7.3)
 
        // 그래프 그릴 자료 넘기기
        LineChart(chartData)
    }
 
    private fun addChartItem(lableitem: String, dataitem: Double) {
        val item = ChartData()
        item.lableData = lableitem
        item.lineData = dataitem
        chartData.add(item)
    }
 
    private fun LineChart(chartData: ArrayList<ChartData>) {
        lineChart = findViewById(R.id.linechart)
 
        val entries = mutableListOf<Entry>()  //차트 데이터 셋에 담겨질 데이터
 
        for (item in chartData) {
            entries.add(Entry(item.lableData.replace(("[^\\d.]").toRegex(), "").toFloat(), item.lineData.toFloat()))
        }
 
        //LineDataSet 선언
        val lineDataSet: LineDataSet
        lineDataSet = LineDataSet(entries, "라인챠트 예시")
        lineDataSet.color = Color.BLUE  //LineChart에서 Line Color 설정
        lineDataSet.setCircleColor(Color.DKGRAY)  // LineChart에서 Line Circle Color 설정
        lineDataSet.setCircleHoleColor(Color.DKGRAY) // LineChart에서 Line Hole Circle Color 설정
 
        val dataSets = ArrayList<ILineDataSet>()
        dataSets.add(lineDataSet) // add the data sets
 
        // create a data object with the data sets
        val data = LineData(dataSets)
 
        // set data
        lineChart.setData(data)
        lineChart.setDescription(null); //차트에서 Description 설정 삭제
 
    }
 
}
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

https://developer.android.com/topic/libraries/architecture/viewmodel 에 잘 설명되어 있다.

ViewModel 를 사용하는 가장 큰 이유는 UI 와 로직의 분리이다.

액티비티, 프래그먼트 생명주기에 종속되지 않게 할 수 있다는 점이 가장 큰 매력이다.

실행되는 앱을 가로모드, 세로모드로 변경하면 값이 초기화되는데 ViewModel 을 사용하면 값이 유지된다.

 

ViewModel에는 onCleared() 함수가 존재한다.

 

jetpack LiveData

Observer에게 데이터 변경에 대한 알림을 보내는 클래스이다.

계속해서 데이터를 관찰하고 업데이트되기 때문에 UI 와 데이터간에 일치성을 가진다는 장점을 가지고 있다.

 

ViewModel 에서 context나 activity객체를 사용하고 싶다면 AndroidViewModel() 사용해야 한다.

 

여러 유투브 동영상을 보고 "개발하는 정대리" https://www.youtube.com/watch?v=-b0VNKw_niY 강좌가 간단하면서도 개념 이해하는데 도움이 되는 거 같아서 이걸 보면서 연습한 코드를 적어둔다.

동영상 강좌 중에 ViewModelProvieders.of(this).get(~~) 이런식으로 설명한 코드가 있는데 테스트 해보니

ViewModelProviders는 deprecated 되었더라. 그러므로 ViewModelProvider를 사용해줘야 한다.

 

앱 build.gradle 추가사항

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
android {
    buildFeatures { // 뷰 바인딩 사용하겠다.
        viewBinding true
    }
}
 
dependencies {
 
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
 
    // 뷰모델 (ViewModel)
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    // 라이브 데이터(LiveData) - 옵저버 패턴 관련 - 데이터의 변경 사항을 알 수 있다.
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}

 

MainViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
 
enum class ActionType {
    PLUS, MINUS
}
 
// 데이터의 변경 : 뷰모델은 데이터의 변경 사항을 알려주는 라이브 데이터를 가지고 있음
class MainViewModel : ViewModel() {
 
    companion object{
        const val TAG: String = "로그"
    }
 
    // 뮤터블 라이브 데이터 - 수정 가능
    // 라이브 데이터 - 값 변경 안됨
 
    // 내부에서 설정하는 자료형은 뮤터블로 변경가능하도록 설정
    private val _currentValue = MutableLiveData<Int>()
 
    // 변경되지 않는 데이터를 가져올 때 이름을 _언더스코어 없이 설정
    // 공개적으로 가져오는 변수는 private 이 아닌 public으로 외부에서도 접근 가능하도록 설정
    // 하지만 값을 직접 라이브데이터에 접근하지 않고 뷰모델을 통해 가져올 수 있도록 설정
    val currentValue: LiveData<Int>
        get() = _currentValue
 
    // 초기값 설정
    init {
        Log.d(TAG, " MainViewModel - 생성자 호출")
        _currentValue.value = 0
    }
 
    fun updateValue(actionType: ActionType, input: Int){
        when(actionType){
            ActionType.PLUS ->
                _currentValue.value = _currentValue.value?.plus(input)
            ActionType.MINUS ->
                _currentValue.value = _currentValue.value?.minus(input)
        }
 
    }
}

 

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MainActivity : AppCompatActivity(), View.OnClickListener {
 
    companion object{
        const val TAG: String = "로그"
    }
 
    val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
 
    // 나중에 값이 설정될 것이라고 lateinit 으로 설정
    lateinit var mainViewModel: MainViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        setContentView(binding.root) // View Bindg 과정
 
        // 뷰 모델 프로바이더를 통해 뷰모델 가져오기
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        // 뷰모델이 가지고 있는 값의 변경사항을 관찰할 수 있는 라이브 데이터를 관찰한다
        mainViewModel.currentValue.observe(this, Observer {
            Log.d(TAG,"MainActivity - mainViewModel - CurrentValue 라이브 데이터 값 변경 : $it")
            binding.tvNumber.text = it.toString()
        })
 
        // 리스너 연결
        binding.btnPlus.setOnClickListener(this)
        binding.btnMinus.setOnClickListener(this)
    }
 
    override fun onClick(view: View?) {
        val userInput: Int  = binding.etNumber.text.toString().toInt()
 
        // ViewModel 에 LiveData 값을 변경하는 메소드
        when(view){
            binding.btnPlus ->
                mainViewModel.updateValue(actionType = ActionType.PLUS, userInput)
            binding.btnMinus ->
                mainViewModel.updateValue(actionType = ActionType.MINUS, userInput)
        }
 
    }
}
 

 

내 GitHub 에 올린 전체 소스 코드

https://github.com/jsk005/KotlinProjects/tree/master/viewmodel

728x90
블로그 이미지

Link2Me

,
728x90

구글에서 검색어 android kotlin view bindg 을 검색하면, https://developer.android.com/topic/libraries/view-binding?hl=ko 를 검색된다.

view binding은 Android Studio 3.6 Canary 11 이상에서 사용할 수 있다.

 

앱 build.gradle 추가사항

android {
        ...
        viewBinding {
            enabled = true
        }
    }

 

또는

android {
    ...
    buildFeatures {
        viewBinding true
   }

}


    

activity_main.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:background="#F5F8FD"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:listitem="@layout/single_item"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

view binding 을 사용하지 않았을 때는

class MainActivity : AppCompatActivity() {

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

    }

view binding 을 사용시에는 setContentView(R.layout.activity_main)  대신에 코드를 변경해야 한다.

view binding 이름을 명명하는 원칙이 있는데, activity_main.xml 에서 첫글자는 대문자로하는 카멜 표기법으로 즉, ActivityMainBinding 으로 한다. 메인 액티비티 --> 액티비티 메인 바인딩

코드는 프래그먼트에서 뷰 결합 사용시와의 일관성 유지를 위한 코드를 작성하는 것이 좋을 듯하다.

아래 샘플 코드는 https://developer.android.com/topic/libraries/view-binding?hl=ko 에 설명된 예제

private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

 

class MainActivity : AppCompatActivity() {
    // view binding for the activity
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    // get reference to the adapter class
    private var languageList = ArrayList<DataItem>()
    private lateinit var expandableAdapter: ExpandableAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 메인 액티비티 --> 액티비티 메인 바인딩
        _binding = ActivityMainBinding.inflate(layoutInflater) // 자동 완성된 액티비티 메인 바인딩 클래스 인스턴스를 가져왔다.
        setContentView(binding.root) // View Bindg 과정

        // define layout manager for the Recycler view
        binding.rvList.layoutManager = LinearLayoutManager(this)
        // attach adapter to the recyclerview
        expandableAdapter = ExpandableAdapter(languageList)

        getData()

        binding.rvList.adapter = expandableAdapter
    }

 

 

위 코드에서 바인딩 방법을 다르게 하는 예제

class MainActivity : AppCompatActivity() {

    val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    // get reference to the adapter class
    private var languageList = ArrayList<DataItem>()
    private lateinit var expandableAdapter: ExpandableAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root) // View Bindg 과정

        // define layout manager for the Recycler view
        binding.rvList.layoutManager = LinearLayoutManager(this)
        // attach adapter to the recyclerview
        expandableAdapter = ExpandableAdapter(languageList)

        getData()

        binding.rvList.adapter = expandableAdapter
    }

 

 

RecyclerView 에서 view binding 하는 사항은 https://stackoverflow.com/questions/60423596/how-to-use-viewbinding-in-a-recyclerview-adapter 의 답변을 참조하거나, https://github.com/jsk005/KotlinProjects/blob/master/expandablerv/src/main/java/com/link2me/expandablerv/ExpandableAdapter.kt 를 참조하면 도움될 것이다.

 

728x90
블로그 이미지

Link2Me

,
728x90
수정 편집을 하려고 하면 에디터가 엉망으로 글을 써주는 통에 적응이 안된다.

fun getMyIPaddress(){
    val gson = GsonBuilder().setLenient().create()
    val retrofit = Retrofit.Builder()
        .baseUrl("http://checkip.amazonaws.com/")
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()
    val service = retrofit.create(RetrofitService.IRetrofit::class.java)

    service.getMyIPAddress().enqueue(object: Callback<String> {
        override fun onResponse(call: Call<String>, response: Response<String>) {
            response.body()?.let {
                PrefsHelper.write("myIPaddress",it) // 평문 저장
                Log.e(TAG,"myIPaddress : ${it}")
            }
        }
        override fun onFailure(call: Call<String>, t: Throwable) {

        }
    })
}



728x90
블로그 이미지

Link2Me

,
728x90

앱의 IP 주소를 알아내는 코드이다.

 

구글 검색하면 가장 많이 나오는 코드

fun getIPAddress(useIPv4: Boolean): String {
    try {
    val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces())
    for (intf in interfaces) {
        val addrs: List<InetAddress> = Collections.list(intf.inetAddresses)
        for (addr in addrs) {
        if (!addr.isLoopbackAddress) {
            val sAddr = addr.hostAddress.toUpperCase()
            val isIPv4 = InetAddressUtils.isIPv4Address(sAddr)
            if (useIPv4) {
            if (isIPv4) return sAddr
            } else {
            if (!isIPv4) {
                val delim = sAddr.indexOf('%') // drop ip6 port suffix
                return if (delim < 0) sAddr else sAddr.substring(0, delim)
            }
            }
        }
        }
    }
    } catch (e: Exception) {
    e.printStackTrace()
    }
    return ""
}
 

My IP Address 앱을 다운로드 받아서 실행해보면, Local IP 로 결과를 반환한다.

 

728x90
블로그 이미지

Link2Me

,
728x90

RSA 암호화 예제를 찾으니 https://travistran.me/rsa-encryption-in-java-and-javascript-1275/ 가 검색되었다.

이 코드를 코틀린으로 변환하여 테스트 해본 것이다.

 

앱 build.gradle 추가사항

// RSA 암호화

implementation 'org.bouncycastle:bcpkix-jdk15on:1.56'

implementation 'javax.xml.bind:jaxb-api:2.2.4'

 

import com.link2me.android.common.TravisRsa.DataTypeEnum
import com.link2me.android.common.TravisRsa.ModeEnum
import kotlin.Throws
import kotlin.jvm.JvmOverloads
import com.link2me.android.common.TravisRsa
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.io.Serializable
import java.lang.Exception
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import javax.xml.bind.DatatypeConverter

class TravisRsa : Serializable {
    enum class ModeEnum {
        PKCS1, OAEP
    }

    enum class DataTypeEnum {
        HEX, BASE64
    }

    var dataType = DataTypeEnum.BASE64
    var mode = ModeEnum.PKCS1
    var privateKey: PrivateKey? = null
    var publicKey: PublicKey? = null

    constructor() {
        try {
            val keyGen = KeyPairGenerator.getInstance("RSA")
            keyGen.initialize(2048)
            val pair = keyGen.generateKeyPair()
            privateKey = pair.private
            publicKey = pair.public
        } catch (e: Exception) {
           
        }
    }

    constructor(keySize: Int) {
        try {
            val keyGen = KeyPairGenerator.getInstance("RSA")
            keyGen.initialize(keySize)
            val pair = keyGen.generateKeyPair()
            privateKey = pair.private
            publicKey = pair.public
        } catch (e: Exception) {
          
        }
    }

    @Throws(Exception::class)
    fun encrypt(plainText: String, publicKey: PublicKey?): ByteArray {
        val cipher = cipher
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)
        return cipher.doFinal(plainText.toByteArray(StandardCharsets.UTF_8))
    }

    @Throws(Exception::class)
    fun decrypt(cipherText: ByteArray?, privateKey: PrivateKey?): ByteArray {
        val cipher = cipher
        cipher.init(Cipher.DECRYPT_MODE, privateKey)
        return cipher.doFinal(cipherText)
    }

    @JvmOverloads
    @Throws(Exception::class)
    fun encrypt(
        plainText: String,
        base64PublicKey: String? = getBase64PublicKey(publicKey!!)
    ): String {
        val cipherText = encrypt(plainText, getPublicKey(base64PublicKey))
        return if (DataTypeEnum.BASE64 == dataType) {
            toBase64(cipherText)
        } else {
            toHex(cipherText)
        }
    }

    @JvmOverloads
    @Throws(Exception::class)
    fun decrypt(
        cipherText: String?,
        base64PrivateKey: String? = getBase64PrivateKey(privateKey!!)
    ): String {
        val cipherBytes: ByteArray
        cipherBytes = if (DataTypeEnum.BASE64 == dataType) {
            fromBase64(cipherText)
        } else {
            fromHex(cipherText)
        }
        return String(decrypt(cipherBytes, getPrivateKey(base64PrivateKey)), StandardCharsets.UTF_8)
    }

    @get:Throws(Exception::class)
    private val cipher: Cipher
        private get() = if (ModeEnum.OAEP == mode) {
            Cipher.getInstance(
                "RSA/ECB/OAEPWithSHA1AndMGF1Padding",
                BouncyCastleProvider()
            )
        } else {
            Cipher.getInstance("RSA/ECB/PKCS1Padding")
        }

    companion object {

        fun getBase64PublicKey(publicKey: PublicKey): String {
            return toBase64(publicKey.encoded)
        }

        fun getBase64PrivateKey(privateKey: PrivateKey): String {
            return toBase64(privateKey.encoded)
        }

        fun getPublicKey(base64PublicKey: String?): PublicKey? {
            try {
                val keySpec = X509EncodedKeySpec(fromBase64(base64PublicKey))
                return KeyFactory.getInstance("RSA").generatePublic(keySpec)
            } catch (e: Exception) {
               
            }
            return null
        }

        fun getPrivateKey(base64PrivateKey: String?): PrivateKey? {
            try {
                val keySpec = PKCS8EncodedKeySpec(fromBase64(base64PrivateKey))
                return KeyFactory.getInstance("RSA").generatePrivate(keySpec)
            } catch (e: Exception) {
               
            }
            return null
        }

        private fun fromBase64(str: String?): ByteArray {
            return DatatypeConverter.parseBase64Binary(str)
        }

        private fun toBase64(ba: ByteArray): String {
            return DatatypeConverter.printBase64Binary(ba)
        }

        private fun fromHex(str: String?): ByteArray {
            return DatatypeConverter.parseHexBinary(str)
        }

        private fun toHex(ba: ByteArray): String {
            return DatatypeConverter.printHexBinary(ba)
        }
    }
}

fun main(args: Array<String>) {
    val input = "안녕하세요. 반갑습니다."
    println(input.length)
    println("byte array length=" + input.toByteArray().size)

    val travisRsa = TravisRsa()
    val encrypted = travisRsa.encrypt(input)
    val decrypted = travisRsa.decrypt(encrypted)

    println("encrypted : $")
    println("decrypted : $")
}
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

코틀린에서 RecyclerViewAdapter를 만드는 방법을 이미지 순서로 만들었다.


class ContactsListAdapter(val itemList: List<Contacts>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {


}








이렇게 하고 나서 아래코드까지 코드를 구현한다.


class ContactsListAdapter(val itemList: List<Contacts>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {

    class ContactsViewHolder(v: View) : RecyclerView.ViewHolder(v) {

    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ContactsListAdapter.ContactsViewHolder {
        TODO("Not yet implemented")
    }

    override fun onBindViewHolder(holder: ContactsListAdapter.ContactsViewHolder, position: Int) {
        TODO("Not yet implemented")
    }

    override fun getItemCount(): Int {
        TODO("Not yet implemented")
    }
}
 


import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.telephony.PhoneNumberUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.link2me.android.sqlite.R
import com.link2me.android.sqlite.model.ContactData
import com.link2me.android.sqlite.utils.Value
import kotlinx.android.synthetic.main.address_item.view.*

class ContactsListAdapter(val context: Context, val itemList: List<ContactData>) : RecyclerView.Adapter<ContactsListAdapter.ContactsViewHolder>() {
    private val TAG = this.javaClass.simpleName

    // itemClockListener 를 위한 인터페이스 정의
    interface OnItemClickListener {
        fun onClick(v: View, position: Int)
    }
    private lateinit var itemClickListener: OnItemClickListener

    fun  setItemClickListener(itemClickListener: OnItemClickListener){
        this.itemClickListener = itemClickListener
    }
    //////////////////////////////////////////////////////////////////

    // RecyclerView 초기화때 호출된다.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactsViewHolder {
        // ViewHolder에 쓰일 Layout을 inflate하는 함수
        val rootView = LayoutInflater.from(parent.context).inflate(R.layout.address_item, parent,false)
        return ContactsViewHolder(rootView,context)
        // 뷰 홀더가 생성된 후 RecyclerView가 뷰 홀더를 뷰의 데이터에 바인딩한다.
    }

    // 생성된 View에 보여줄 데이터를 설정
    override fun onBindViewHolder(holder: ContactsViewHolder, position: Int) {
        // RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출한다.
        val item = itemList[position]

        holder.itemView.setOnClickListener {
            itemClickListener.onClick(it,position)
        }

        holder.apply {
            bind(item)
        }
    }

    override fun getItemCount(): Int = itemList.size

    class ContactsViewHolder(var view: View, val context: Context) : RecyclerView.ViewHolder(view) {
        // View를 저장할 수 있는 변수
        val img_photo = view.findViewById<ImageView>(R.id.profile_Image)
        val tv_name = view.findViewById<TextView>(R.id.child_name)
        val tv_mobileNO = view.findViewById<TextView>(R.id.child_mobileNO)
        val btn_phonecall = view.findViewById<ImageView>(R.id.child_Btn)

        // View와 데이터를 연결시키는 함수
        fun bind(item: ContactData){
            if (item.photo.contains("jpg")){
                val photoURL = Value.Photo_URL + item.photo
                Glide.with(context).load(photoURL).into(img_photo)
            }

            tv_name.text = item.userNM
            tv_mobileNO.text = item.mobileNO

            val items = arrayOf("휴대폰 전화걸기", "연락처 저장")
            val builder = AlertDialog.Builder(context)
            builder.setTitle("해당작업을 선택하세요")
            builder.setItems(items, DialogInterface.OnClickListener { dialog, which ->
                Toast.makeText(context, items[which] + "선택했습니다.", Toast.LENGTH_SHORT).show()
                when (which) {
                    0 -> {
                        if (item.mobileNO.length == 0) {
                            Toast.makeText(context, "전화걸 휴대폰 번호가 없습니다.", Toast.LENGTH_SHORT).show()
                            return@OnClickListener
                        }
                        val dialog1 = AlertDialog.Builder(context)
                            .setTitle(item.userNM)
                            .setMessage(PhoneNumberUtils.formatNumber(item.mobileNO) + " 통화하시겠습니까?")
                            .setPositiveButton("예",
                                { dialog, which ->
                                    val intent = Intent(Intent.ACTION_CALL,
                                        Uri.parse("tel:" + PhoneNumberUtils.formatNumber(item.mobileNO))
                                    )
                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    context.startActivity(intent)
                                })
                            .setNegativeButton("아니오") { dialog, which -> dialog.dismiss() }.create()
                        dialog1.show()
                    }
                    1 -> Toast.makeText(context, "전화번호 저장하는 로직은 직접 구현하시기 바랍니다.", Toast.LENGTH_SHORT).show()
                }
            })
            builder.create()
            btn_phonecall.setOnClickListener { builder.show() }
        }
    }
}
 


사용 예제에 대한 전체 코드는 GitHub 에 올려뒀다.

https://github.com/jsk005/KotlinProjects/tree/master/retrofit


728x90
블로그 이미지

Link2Me

,
728x90

코틀린에서 Retrofit2 라이브러리를 이용하여 서버 데이터를 가져오는 기능을 처리하는데 헷갈려서 정리를 좀 해둔다.


예제1)


먼저 PHP 코드에서 JSON 데이터를 만드는 방법

    $R = array(); // 결과 담을 변수 생성
    $result = $c->putDbArray($sql);
    while($row = $result->fetch_assoc()) {
        if($row['photo'] == NULL) {
            $row['photo'] = "";
        } else {
            $path = "./photos/".$row['photo'];
            if(!file_exists($path)) {
                $row['photo'] = "";
            }
        }
        array_push($R, $row);
    }
    header("Cache-Control: no-cache, must-revalidate");
    header("Content-type: application/json; charset=UTF-8");

    $status = "success";
    $result = array(
        'status' => $status,
        'message' => $R
    );
    echo json_encode($result);
 

while 문을 사용했다는 것은 $R 변수는 배열이라는 것을 명심하자.


서버에서 받은 데이터를 Retrofit 라이브러리에서 받아서 처리하기 위한 data class

@Parcelize
data class ContactData (
    // PersonData 정보를 담고 있는 객체 생성
    var idx: String,
    var userNM: String = "",
    var mobileNO: String = "",
    var officeNO: String = "",
    var photo: String = "", // 이미지 경로를 String으로 받기 위해서
    var isCheckBoxState: Boolean = false
): Parcelable
 

@Parcelize
class ContactDataResult (
    val status: String,
    val message: List<ContactData>
): Parcelable

interface IRetrofit {

    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactDataResult(@Field("idx") idx: String): Call<ContactDataResult>
}
 


Retrofit 라이브러리 사용법

object RetroClient { // 싱글턴
    // 레트로핏 클라어언트 선언
    private var retrofitClient: Retrofit? = null
    var instance: IRetrofit? = null

    val gson = GsonBuilder().setLenient().create()

    // 레트로핏 클라어언트 가져오기
    fun getClient(baseUrl: String): Retrofit? {
        if (retrofitClient == null){
            retrofitClient = Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .client(Utils.createOkHttpClient())
                .build()
        }
        return retrofitClient
    }

    @JvmName("getInstance1")
    fun getInstance(): IRetrofit? {
        if (instance == null) {
            instance = getClient(Value.API_BASE_URL)?.create(IRetrofit::class.java)
        }
        return instance
    }

}
 

class Value : AppCompatActivity() {
    companion object{
        const val API_BASE_URL = "https://test.abc.com/androidSample/"
        const val Photo_URL = "https://test.abc.com/androidSample/photos/"

        const val Contacts = "getContactData.php"
    }
}
 

   private fun getServerData() {
        RetroClient.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataResult> {
            override fun onResponse(call: Call<ContactDataResult>, response: Response<ContactDataResult>) {
                if (response.body()!!.status.contains("success")){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!.message
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<ContactDataResult>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })

    }
 



예제2)

PHP 코드 수정사항

echo json_encode(array('message'=>$R));


결과 형태

{"message":[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
{"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
{"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
{"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"}]}
 


코틀린 수정사항

@Parcelize
class ContactDataResult (
    val message: List<ContactData>
): Parcelable

// 수정사항 없음
@Parcelize
data class ContactData (
    // PersonData 정보를 담고 있는 객체 생성
    var idx: String,
    var userNM: String = "",
    var mobileNO: String = "",
    var officeNO: String = "",
    var photo: String = "", // 이미지 경로를 String으로 받기 위해서
    var isCheckBoxState: Boolean = false
): Parcelable
 


        RetroClient.getInstance()?.getContactDataResult("1")?.enqueue(object :
            Callback<ContactDataResult> {
            override fun onResponse(call: Call<ContactDataResult>, response: Response<ContactDataResult>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!.message
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<ContactDataResult>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })
 


예제3)

PHP 코드 수정사항

echo json_encode($R);


JSON 결과==> 배열

[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
 {"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
 {"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
 {"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"},
 {"idx":5,"userNM":"\ud64d\uae38\ub3d9","mobileNO":"01000009880","telNO":"","photo":"5.jpg"}]
 


코틀린 수정사항


interface IRetrofit {
    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactsList(@Field("idx") idx: String): Call<List<ContactData>>
}
 



        RetroClient.getInstance()?.getContactsList("1")?.enqueue(object :
            Callback<List<ContactData>> {
            override fun onResponse(call: Call<List<ContactData>>, response: Response<List<ContactData>>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val contactData = response.body()!!
                    for (item in contactData){
                        addressItemList.add(item)
                        searchItemList.add(item)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<List<ContactData>>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })
 



예제4) JSON 파싱하기
PHP 코드 수정사항
echo json_encode(array('result'=>$R));

JSON 결과

{"result":[{"idx":1,"userNM":"\uac1c\ubc1c\uc790","mobileNO":"01000010001","telNO":"0234560001","photo":"1.jpg"},
           {"idx":2,"userNM":"\uc774\uc815\uc740","mobileNO":"01001230001","telNO":"","photo":"2.jpg"},
       {"idx":3,"userNM":"\uae40\ud64d\uae38","mobileNO":"01001230002","telNO":"","photo":""},
       {"idx":4,"userNM":"\ucd5c\uc2e0\ud615","mobileNO":"01001230003","telNO":"","photo":"4.jpg"},
       {"idx":5,"userNM":"\ud64d\uae38\ub3d9","mobileNO":"01000009880","telNO":"","photo":"5.jpg"}]}
 


코틀린 수정사항

interface IRetrofit {
    @FormUrlEncoded
    @POST(Value.Contacts)
    fun getContactsObject(@Field("idx") idx: String): Call<JsonObject>
} 

object ContactContract {
    const val _RESULTS = "result" // 서버 정보를 파싱하기 위한 변수 선언

    object Entry : BaseColumns {
        const val TABLE_NAME = "PBbook"
        const val _IDX = "idx" // 서버 테이블의 실제 필드명
        const val _NAME = "userNM"
        const val _MobileNO = "mobileNO"
        const val _telNO = "telNO"
        const val _Team = "Team"
        const val _Mission = "Mission"
        const val _Position = "Position"
        const val _Photo = "photo" // 이미지 필드
        const val _Status = "status"
    } // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
}
 

=== MainActivity.kt 파일에서 발췌 ===

    private fun getServerData() {
        RetroClient.getInstance()?.getContactsObject("1")?.enqueue(object :
            Callback<JsonObject> {
            override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
                if (response.isSuccessful){
                    addressItemList.clear()
                    searchItemList.clear()

                    val jsonObj = JSONObject(response.body().toString())
                    val person = jsonObj.getJSONArray(ContactContract._RESULTS)

                    for (i in 0 until person.length()) {
                        val c = person.getJSONObject(i)
                        val idx = c.getString(ContactContract.Entry._IDX)
                        val name = c.getString(ContactContract.Entry._NAME)
                        val mobileNO = c.getString(ContactContract.Entry._MobileNO)
                        val officeNO = c.getString(ContactContract.Entry._telNO)
                        val PhotoImage = c.getString(ContactContract.Entry._Photo)

                        Log.e(TAG,"name : " + name)

                        getAllDataList(idx, name, mobileNO, officeNO, PhotoImage, false)
                        selectDataList(idx, name, mobileNO, officeNO, PhotoImage, false)
                    }

                    runOnUiThread { // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged()
                    }
                }
            }

            override fun onFailure(call: Call<JsonObject>, t: Throwable) {
                Log.e(TAG,"Retrofit response fail.")
            }

        })

    }
 
    // 아이템 전체 데이터 추가 메소드
    fun getAllDataList(uid: String, name: String, mobileNO: String, officeNO: String, photo_image: String, checkItem_flag: Boolean) {
        val item = ContactData(uid, name, mobileNO, officeNO, photo_image, checkItem_flag)
        addressItemList.add(item)
    }

    // 선택한 데이터 추가 메소드
    fun selectDataList(uid: String, name: String, mobileNO: String, officeNO: String, photo_image: String, checkItem_flag: Boolean) {
        val item = ContactData(uid, name, mobileNO, officeNO, photo_image, checkItem_flag)
        searchItemList.add(item)
    }
 




728x90
블로그 이미지

Link2Me

,
728x90

Kotlin으로 SQLite를 활용해서 db생성, insert문, delete문, update문, select문 사용 방법에 대한 예제다.
서버에 있는 이미지는 URL 경로를 Glide 라이브러리를 이용하여 보여주는 방법으로 구현했다.


프로젝트 build.gradle

buildscript {
    ext.kotlin_version = "1.4.21"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        def nav_version = "2.3.2"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }

    }
}

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



앱 build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'  // Android Studio 4.1부터 사용하지 말라고 권고하고 ViewBindg 사용 권장
    id 'kotlin-kapt'
}

// ViewBinding 은 Android Studio 3.6부터 사용 가능

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

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

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

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'

    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'

    implementation 'com.github.bumptech.glide:glide:4.11.0'
    kapt 'com.github.bumptech.glide:compiler:4.11.0'

    // debugging 을 위한 툴
    implementation 'com.facebook.stetho:stetho:1.5.1'
    implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
    implementation 'com.facebook.stetho:stetho-js-rhino:1.5.1' // 콘솔을 이용한 명령어 입력기능

}
 


ContactContract.kt

import android.provider.BaseColumns

object ContactContract {
    const val _RESULTS = "result" // 서버 정보를 파싱하기 위한 변수 선언
    const val _CallingNO = "callingNO" // 전화 수신
    const val _CallingTime = "received_time"

    object Entry : BaseColumns {
        // BaseColumns 인터페이스를 구현함으로써 내부 클래스는 _ID라고 하는 기본 키 필드를 상속할 수 있다.
        const val TABLE_NAME = "PBbook"
        const val _IDX = "idx" // 서버 테이블의 실제 필드명
        const val _NAME = "userNM"
        const val _MobileNO = "mobileNO"
        const val _telNO = "telNO"
        const val _Team = "Team"
        const val _Mission = "Mission"
        const val _Position = "Position"
        const val _Photo = "photo" // 이미지 필드
        const val _Status = "status"
    } // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
}
 



ContactDBHelper.kt

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
import android.util.Log

// 싱글톤 패턴 구현 시, 해당 클래스의 생성자는 private 로 선언하여 외부에서의 직접 접근을 막아야 한다.
class ContactDBHelper private constructor(context: Context) :
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    private val TAG = this.javaClass.simpleName
    override fun onCreate(db: SQLiteDatabase) {
        // db.execSQL(String query) 는 입력한 쿼리문을 실행하는 메소드.
        db.execSQL(SQL_CREATE_ENTRIES)
        Log.v(TAG, "DB Created")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // onUpgrade 콜백 메서드는 DB의 스키마가 변경되어 DB 버전이 올라갔을 때 호출된다.
        // DB의 스키마가 변경되면 DB의 버전을 올려야 한다.
        db.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db) //새로 생성하기
    }

    companion object {
        // 먼저 db 파일을 만들어야 한다. db 파일에 관련 테이블들을 생성 및 저장
        private const val DATABASE_VERSION = 1 // 데이터베이스의 버전. 스키마가 변경될 때 숫자를 올린다.
        private const val DATABASE_NAME = "orgChart.db"

        // 테이블을 생성하는 쿼리
        private val SQL_CREATE_ENTRIES = "create table ${ContactContract.Entry.TABLE_NAME} (" +
                "${BaseColumns._ID } INTEGER PRIMARY KEY AUTOINCREMENT," +
                "${ContactContract.Entry._IDX} TEXT not null unique," +
                "${ContactContract.Entry._NAME} TEXT not null," +
                "${ContactContract.Entry._MobileNO} TEXT not null," +
                "${ContactContract.Entry._telNO} TEXT," +
                "${ContactContract.Entry._Team} TEXT," +
                "${ContactContract.Entry._Mission} TEXT," +
                "${ContactContract.Entry._Position} TEXT," +
                "${ContactContract.Entry._Photo} TEXT," +
                "${ContactContract.Entry._Status} TEXT);"

        // SQLite의 데이터 타입은 NULL, INTEGER, REAL, TEXT, BLOB 만 지원한다.
        // DB 생성 위치 : /data/data/<application-package-name>/databases/<database-name>
        private const val SQL_DELETE_ENTRIES =
            "DROP TABLE IF EXISTS ${
ContactContract.Entry.TABLE_NAME}"

        // db 헬퍼는 싱글톤 패턴으로 구현하는 것이 좋다.
        // 싱글톤 패턴은 프로그램 내에서 객체가 1개로 고정되게 하는 패턴.
        private var sInstance: ContactDBHelper? = null

        // 싱글톤 패턴을 구현할 때, 주로 메소드를 getInstance 로 명명한다.
        // 여러 곳에서 동시에 db 를 열면 동기화 문제가 생길 수 있기 때문에 synchronized 키워드를 이용한다.
        @Synchronized
        fun getInstance(context: Context): ContactDBHelper? {
            if (sInstance == null) {  // 객체가 없을 경우에만 객체를 생성한다.
                sInstance = ContactDBHelper(context)
            }
            return sInstance // 객체가 이미 존재할 경우, 기존 객체를 리턴.
        }
    }
}
 


전체 소스코드는 Github에 올려두었다.

https://github.com/jsk005/KotlinProjects/tree/master/sqlite


728x90
블로그 이미지

Link2Me

,
728x90

Activity간에 Data를 묶어서 전달해야 할 경우가 있다.

자바로 구현한 예제를 코틀린으로 변환하면 일단 에러가 난다.

import android.os.Parcel;
import android.os.Parcelable;

public class Profiles implements Parcelable {
    // Parcelable 인터페이스 : 커스텀 클래스나 오브젝트를 다른 컴포넌트에 전달해 줘야 할 경우
    // A Activity에서 B Activity로 데이터를 전달할 때, 데이터를 묶어서 전달한다
    // https://developer.android.com/reference/android/os/Parcelable 참조
    // Android에서 성능적인 이슈로 Serializable 보다는 Parcelable를 사용하는걸 추천한다.

    String name;
    int age;
    String gender;

    public Profiles() {
        // 모든 클래스는 반드시 생성자를 가져야 한다.
        // 생성자가 하나도 없을 때만, 컴파일러가 default constructor(기본 생성자) 자동 추가
        // 기본 생성자 : 매개변수가 없는 생성자
    }

    public Profiles(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    //Parcel은 데이터를 전달할 때 사용되는 객체
    //Parcel 데이터를 읽어서 변수에 할당
    public Profiles(Parcel src) {
        name = src.readString();
        age = src.readInt();
        gender = src.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flag) {
        dest.writeString(name);
        dest.writeInt(age);
        dest.writeString(gender);
    }

    public static final Creator<Profiles> CREATOR = new Creator<Profiles>() {

        @Override
        public Profiles createFromParcel(Parcel parcel) {
            return new Profiles(parcel);
        }

        @Override
        public Profiles[] newArray(int size) {
            //배열 객체 만들어 주기
            return new Profiles[size];
        }
    };
}
 


Android Studio가 제공하는 파일 변환 기능으로 코틀린으로 변환하고 약간 수작업으로 수정한 예제

import android.os.Parcel
import android.os.Parcelable
import kotlinx.android.parcel.Parceler
import kotlinx.android.parcel.Parcelize

@Parcelize
class Profiles() : Parcelable {
    // Parcelable 인터페이스 : 커스텀 클래스나 오브젝트를 다른 컴포넌트에 전달해 줘야 할 경우
    // A Activity에서 B Activity로 데이터를 전달할 때, 데이터를 묶어서 전달한다
    // Android에서 성능적인 이슈로 Serializable 보다는 Parcelable를 사용하는걸 추천한다.
    var name: String? = null
    var age = 0
    var gender: String? = null

    constructor(name: String?, age: Int, gender: String?) : this() {
        this.name = name
        this.age = age
        this.gender = gender
    }

    // Parcel은 데이터를 전달할 때 사용되는 객체
    // Parcel 데이터를 읽어서 변수에 할당
    constructor(parcel: Parcel) : this() {
        parcel.run {
            name = readString()
            age = readInt()
            gender = readString()
        }
    }

    override fun describeContents(): Int = 0

    companion object : Parceler<Profiles> {
        override fun Profiles.write(parcel: Parcel, flag: Int) {
            parcel.writeString(name)
            parcel.writeInt(age)
            parcel.writeString(gender)
        }

        override fun create(parcel: Parcel): Profiles {
            return Profiles(parcel)
        }
    }
}
 



그런데 이런 변환 과정을 할 필요없이 아주 간단하게 해결할 수 있다.

https://developer.android.com/kotlin/parcelize 를 참조하자.


앱 build.gradle 추가사항

androidExtensions {
    experimental = true
}


@Parcelize annotation을 사용하면, writeToParcel()/createFromParcel()를 자동으로 구현해준다.

아래 코드를 보라. 얼마나 간단한가.

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Profiles(
    var name: String="",
    var age:Int = 0,
    var gender: String=""
):Parcelable
 



Activity 이동전

val intent = Intent(this@MainActivity, TargetActivity::class.java)
intent.putExtra("parcel", profile)
startActivity(intent)


Activity 이동후

val profile: Profiles = intent.getParcelableExtra("parcel") 



728x90
블로그 이미지

Link2Me

,
728x90

자바와 코틀린의 기능을 표로 간략하게 비교하는 걸로 자주 사용하는 걸 추가해 두고자 한다.


기능

언어

설명

Intent

Java

  Intent intent = new Intent(getApplicationContext(), SubActivity.class);
  intent.putExtra("name","홍길동");
  startActivity(intent);

  Intent intent = getIntent(); /*데이터 수신*/
  String name = intent.getExtras().getString("name");
  int age = intent.getExtras().getInt("age");

kotlin

  val intent = Intent(this, secondActivity::class.java)
  intent.putExtra("phone",""010-0000-0000")
  startActivity(intent)

  val intent = getIntent()
  val phone =intent.getStringExtra("phone").toString()


기능

언어

설명

Context

Java

 Context mContext;
 mContext= LoginActivity.this;

kotlin

 lateinit var mContext: Context
 mContext = this@LoginActivity


기능

언어

설명

TAG

Java

 private final String TAG = this.getClass().getSimpleName();

kotlin

 private val TAG = this.javaClass.simpleName


728x90
블로그 이미지

Link2Me

,
728x90

Java static 메서드가 포함된 Class를 코틀린에서 직접 호출하니까 에러가 발생하고 제대로 동작이 안된다.


그래서 Android Studio 에서 기본 제공하는 방법으로 파일을 변환하면 Object 파일로 변환을 한다.

이렇게 변환한 파일을 다시 수작업으로 companion object 안에 메서드를 감싸는 방법으로 해주는 것이 좋더라.


자바 코드

public class Utils extends AppCompatActivity {

    public static void showAlert(Context context, String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    public static OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }

    public static boolean isValidmobileNO(String cellphoneNumber) {
        boolean returnValue = false;
        Log.i("cell", cellphoneNumber);
        String regex = "^\\s*(010|011|012|013|014|015|016|017|018|019)(-|\\)|\\s)*(\\d{3,4})(-|\\s)*(\\d{4})\\s*$";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(cellphoneNumber);
        if (m.matches()) {
            returnValue = true;
        }
        return returnValue;
    }

    public static int getVersionCode(Context context) {
        PackageInfo packageInfo = null;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        int versionCode = packageInfo.versionCode;
        return versionCode;
    }

}
 



코틀린 변환 코드

class Utils : AppCompatActivity() {

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

        fun createOkHttpClient(): OkHttpClient {
            // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
            val builder = OkHttpClient.Builder()
            val interceptor = HttpLoggingInterceptor()
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
            builder.addInterceptor(interceptor)
            return builder.build()
        }

        fun isValidmobileNO(cellphoneNumber: String?): Boolean {
            var returnValue = false
            Log.i("cell", cellphoneNumber!!)
            val regex =
                "^\\s*(010|011|012|013|014|015|016|017|018|019)(-|\\)|\\s)*(\\d{3,4})(-|\\s)*(\\d{4})\\s*$"
            val p = Pattern.compile(regex)
            val m = p.matcher(cellphoneNumber)
            if (m.matches()) {
                returnValue = true
            }
            return returnValue
        }

        fun getVersionCode(context: Context): Int {
            var packageInfo: PackageInfo? = null
            try {
                packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            } catch (e: PackageManager.NameNotFoundException) {
                e.printStackTrace()
            }
            val versionCode = packageInfo!!.versionCode
            return versionCode
        }

}


안드로이드 스튜디오가 기본 제공하는 변환을 이용하면, 약간 코틀린 문법에 맞게 변수를 조정해준다.

이렇게 하고 나서 코틀린에서 해당 메서드를 호출하면 정상으로 잘 동작한다.


728x90
블로그 이미지

Link2Me

,
728x90

Java 코드로 만든 코드를 코틀린으로 변환하고 테스트를 하니까 제대로 동작이 안된다.

코드 일부를 수정해주고 나서 동작에 제대로 된다.

Volley 라이브리를 사용하면 간단하게 해결될 사항이지만 HttpURLConnection 으로 서버 데이터를 안드로이드폰으로 가져오는 방법도 알아두면 좋을 거 같아서 테스트하고 적어둔다.


private fun UpgradeChk() {
     val builder = Uri.Builder()
         .appendQueryParameter("os", "a")
     val postParams = builder.build().encodedQuery
     val getlastVersion = getlastVersion()
     getlastVersion.execute( Value.IPADDRESS + "/lastVersion.php", postParams)
 }

inner class getlastVersion : AsyncTask<String, Void, String>() {
    override fun doInBackground(vararg params: String): String {
        return try {
            HttpURLComm.getJson(params[0], params[1])
        } catch (e: Exception) {
            String.format("Exception: " + e.message)
        }
    }

    override fun onPostExecute(response: String) {
        version = Value.VERSION // 앱 버전
        version = version.replace("[^0-9]".toRegex(), "") // 버전에서 숫자만 추출
        Log.e("WEB", "Response: $response")
        val Response = response.replace("[^0-9]".toRegex(), "") // 버전에서 숫자만 추출
        println("Server Version : $Response")
        if (version.toInt() < Response.toInt()) { // 서버 버전이 더 높으면
            UpgradeProcess()
        } else {
            AutoLoginProgress()
        }
    }
}
 

import android.util.Log
import android.webkit.CookieManager
import com.link2me.android.enode.Value
import java.io.BufferedReader
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL

object HttpURLComm  {
    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    @JvmStatic
    @Throws(Exception::class)
    fun getJson(serverUrl: String?, postParams: String?): String {
        try {
            Thread.sleep(100)
            val url = URL(serverUrl)
            val conn = url.openConnection() as HttpURLConnection
            // 세션 쿠키 전달
            val cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS)
            val sb = StringBuilder()
            if (conn != null) { // 연결되었으면
                //add request header
                conn.requestMethod = "POST"
                conn.setRequestProperty("USER-AGENT", "Mozilla/5.0")
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
                conn.setRequestProperty("Accept-Language", "en-US,en;q=0.5")
                if (cookieString != null) {
                    conn.setRequestProperty("Cookie", cookieString)
                    Log.e("PHP_getCookie", cookieString)
                }
                conn.connectTimeout = 10000
                conn.readTimeout = 10000
                conn.useCaches = false
                conn.defaultUseCaches = false
                conn.doOutput = true // POST 로 데이터를 넘겨주겠다는 옵션
                conn.doInput = true

                // Send post request
                val wr = DataOutputStream(conn.outputStream)
                wr.writeBytes(postParams)
                wr.flush()
                wr.close()
                val responseCode = conn.responseCode
                Log.e("TAG","GET Response Code : $responseCode")
                if (responseCode == HttpURLConnection.HTTP_OK) { // 연결 코드가 리턴되면
                    val allText: String = conn.inputStream.bufferedReader().use(BufferedReader::readText)
                    sb.append(allText.trim())
                }
            }
            conn.disconnect()
            return sb.toString()
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {

        }
        return ""
    }
}


https://github.com/irontec/android-kotlin-samples/blob/master/KotlinTest/app/src/main/java/com/irontec/examples/kotlintest/HttpUrlConnectionAsyncActivity.kt

를 참조하여 코드를 약간 보완했다.

object HttpURLComm {
    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    @JvmStatic
    @Throws(Exception::class)
    fun getJson(serverUrl: String?, postParams: String?): String {
        Thread.sleep(100)
        val url = URL(serverUrl)
        val httpClient = url.openConnection() as HttpURLConnection
        // 세션 쿠키 전달
        val cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS)
        val sb = StringBuilder()
        if (httpClient != null) { // 연결되었으면
            //add request header
            httpClient.requestMethod = "POST"
            httpClient.setRequestProperty("USER-AGENT", "Mozilla/5.0")
            httpClient.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            httpClient.setRequestProperty("Accept-Language", "en-US,en;q=0.5")
            if (cookieString != null) {
                httpClient.setRequestProperty("Cookie", cookieString)
                Log.e("PHP_getCookie", cookieString)
            }
            httpClient.connectTimeout = 10000
            httpClient.readTimeout = 10000
            httpClient.useCaches = false
            httpClient.defaultUseCaches = false
            httpClient.doOutput = true // POST 로 데이터를 넘겨주겠다는 옵션
            httpClient.doInput = true

            // Send post request
            val wr = DataOutputStream(httpClient.outputStream)
            wr.writeBytes(postParams)
            wr.flush()
            wr.close()

            if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                try {
                    val stream = BufferedInputStream(httpClient.inputStream)
                    val data: String = readStream(inputStream = stream)
                    return data
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    httpClient.disconnect()
                }
            } else {
                println("ERROR ${httpClient.responseCode}")
            }
        }
        return ""
    }

    fun readStream(inputStream: BufferedInputStream): String {
        val bufferedReader = BufferedReader(InputStreamReader(inputStream))
        val stringBuilder = StringBuilder()
        bufferedReader.forEachLine { stringBuilder.append(it) }
        return stringBuilder.toString()
    }
}
 



728x90
블로그 이미지

Link2Me

,
728x90

위험권한을 편리하게 체크해주는 tedpermission 라이브러리를 코틀린으로 변환한 것과 뒤로 가기를 두번 연속 누르면 종료되는 함수를 적어둔다.

구글지도를 간단하게 구현할 목적이라 dependencies에 구글 라이브러리가 추가되어 있다.

 

앱 build.gradle

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

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

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

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

}

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

    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.google.android.material:material:1.1.0'

}

2021.7월 확인 사항

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

 

AndroidManifest.xml

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

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

    <!-- 도시 블록 내에서 정확한 위치(네트워크 위치) -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <!-- 정확한 위치 확보(네트워크 위치 + GPS 위치) -->

    <application
        android:allowBackup="false"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true">
        <activity android:name=".MapsActivity"></activity>
        <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>

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="@string/google_maps_key" />

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>
    </application>

</manifest>

 

SplashActivity.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.os.Handler
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 SplashActivity : AppCompatActivity() {
    var mContext: Context? = null
    private val SPLASH_TIME_OUT: Long = 2000 // 2 sec

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mContext = this@SplashActivity;
        checkPermissions()
    }

    var permissionlistener: PermissionListener = object : PermissionListener {
        override fun onPermissionGranted() {
            initView()
        }

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

    private fun checkPermissions() {
        if (Build.VERSION.SDK_INT >= 26) { // 출처를 알 수 없는 앱 설정 화면 띄우기
            val pm: PackageManager = mContext!!.getPackageManager()
            Log.e("PackageName", 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(mContext)
                .setPermissionListener(permissionlistener)
                .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                .setPermissions(
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                )
                .check()
        } else {
            initView()
        }
    }

    private fun initView() {
        Handler().postDelayed({
            startActivity(Intent(this, MapsActivity::class.java))
            finish()  // close this activity
        }, SPLASH_TIME_OUT)
    }

}

 

import android.app.Activity
import android.os.Build
import android.widget.Toast

class BackPressHandler(private val activity: Activity) {
    private var backKeyPressedTime: Long = 0
    private var toast: Toast? = null
    fun onBackPressed() {
        if (isAfter2Seconds) {
            backKeyPressedTime = System.currentTimeMillis()
            // 현재시간을 다시 초기화
            toast = Toast.makeText(
                activity,
                "\'뒤로\'버튼을 한번 더 누르면 종료됩니다.",
                Toast.LENGTH_SHORT
            )
            toast!!.show()
            return
        }
        if (isBefore2Seconds) {
            appShutdown()
            toast!!.cancel()
        }
    }

    // 2초 지났을 경우
    private val isAfter2Seconds: Boolean
        private get() = System.currentTimeMillis() > backKeyPressedTime + 2000

    // 2초가 지나지 않았을 경우
    private val isBefore2Seconds: Boolean
        private get() = System.currentTimeMillis() <= backKeyPressedTime + 2000

    private fun appShutdown() {
        // 홈버튼 길게 누르면 나타나는 히스토리에도 남아있지 않고 어플 종료
        if (Build.VERSION.SDK_INT >= 21) activity.finishAndRemoveTask() else activity.finish()
        System.exit(0)
    }
}

 

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MapsActivity : AppCompatActivity() {
    private var backPressHandler: BackPressHandler? = null

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

        backPressHandler = BackPressHandler(this); // 뒤로 가기 버튼 이벤트
    }

    override fun onBackPressed() {
        backPressHandler!!.onBackPressed()
    }
}

 

var a = readLine()?.capitalize()
// ?. : 안전 호출 연산자


var a = readLine()!!.capitailize()
// !! : non-null 단언 연산자. null이 될 수 없다는 것을 단언하는 연산자다.

 

728x90
블로그 이미지

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()
}
728x90
블로그 이미지

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()
        }
    }

}


728x90
블로그 이미지

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() {

    }
}

 

728x90

'안드로이드 > 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

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))


728x90
블로그 이미지

Link2Me

,