buildTypes { debug { minifyEnabled false // 2022.06.23 true 가 되었더니, 압축이 되서 null 오류가 난다. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } release { minifyEnabled false // 2022.06.23 true 가 되었더니, 압축이 되서 null 오류가 난다. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } viewBinding{ enabled = true }
def archiveBuildType = ["release"] namespace 'com.nari.notify2kakao' applicationVariants.all { variant -> variant.outputs.each { output -> if (variant.buildType.name in archiveBuildType) { def df = new SimpleDateFormat("yyyyMMdd") df.setTimeZone(TimeZone.getDefault()) if (variant.versionName != null) { String name = "Notify2Kakao_${df.format(new Date())}_${defaultConfig.versionCode}_${variant.versionName}.apk" output.outputFileName = name } } } } }
dependencies {
constraints { // 추가한 부분 implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") } implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") }
implementation('androidx.work:work-runtime:2.7.1') { because '''androidx.work:work-runtime:2.1.0 pulled from play-services-ads has a bug using PendingIntent without FLAG_IMMUTABLE or FLAG_MUTABLE and will fail in apps targeting S+.''' } }
implementation("androidx.core:core-ktx:1.10.1") // 추가한 부분 implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") // 추가한 부분
implementation "com.kakao.sdk:v2-all:2.15.0" // 전체
// 데이터 주고 받기 implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-simplexml:2.9.0'
}
실행하는 방법은 메뉴에서 Code 메뉴 아래에 보면 Convert Java File To Kotlin File 메뉴가 있는 데, 저는 아래 그림처럼 변환할 대상을 찾고 오른쪽 마우스 클릭해서 해 보도록 하겠습니다.
변환해 보기
이제 java 코드를 kotlin 으로 하나씩 변환해 보겠습니다.
변환하면서 쉽게 만나는 오류 표시
이름 그림과 같이 kotlin으로 변환된 뒤에 변수 처리 등에 오류 표시가 나오는 경우를 종종 만나게 됩니다. 사유는 해당 변수를 변환하면서 null 허용 상태를 만들어 가면서 변환을 하기 때문입니다. 선언된 변수를 찾아보겠습니다.
이렇게 null 로 초기화를 해야만 하는 가 봅니다. kotlin 에는 초기화를 나중에 하는 유용한 방법이 있는 데도 말입니다. 그래서 다음과 같이 변경을 해 주면 해결이 됩니다.
이제 소스 코드에 표시 되었던 오류는 사라집니다. 다만, 실제로 source 코드에서는 해당 변수를 사용하기 전에 꼭 초기화를 해야 실행 시 오류가 발생하지 않습니다.
java.lang.NullPointerException: Parameter specified as non-null is null:
또한 java.lang.NullPointerException: Parameter specified as non-null is null 이런 오류를 만나게 되었습니다. 사유는 Recycle View을 이용해서 List에 들어 있는 항목들을 보여주는 화면들이 있는 데, View Adapter 등을 사용했었다면 만나게 될 것 같습니다.
Process: com.nari.notify2kakao, PID: 17268 java.lang.NullPointerException: Parameter specified as non-null is null: method com.nari.notify2kakao.util.ViewStrValueAdapter.getView, parameter convertView at com.nari.notify2kakao.util.ViewStrValueAdapter.getView(Unknown Source:2) at android.widget.AbsListView.obtainView(AbsListView.java:2466) at android.widget.ListView.makeAndAddView(ListView.java:2065) at android.widget.ListView.fillDown(ListView.java:791) at android.widget.ListView.fillFromTop(ListView.java:853) at android.widget.ListView.layoutChildren(ListView.java:1836) at android.widget.AbsListView.onLayout(AbsListView.java:2263) at android.view.View.layout(View.java:24421)
아래 Adapter 예시와 같이 getView 의 리턴값이 View? 이 될 수 있도록 조치를 해 주시면 해소가 됩니다.
class ViewWithDrawMonthlyAdapter(oData: ArrayList<ViewWithDrawMonthly>) : BaseAdapter() { private var viewWithDrawMonthlyls = ArrayList<ViewWithDrawMonthly>()
init { viewWithDrawMonthlyls = oData }
override fun getCount(): Int { return viewWithDrawMonthlyls.size }
override fun getItem(position: Int): Any { return viewWithDrawMonthlyls[position] }
override fun getItemId(position: Int): Long { return position.toLong() }
override fun getView(position: Int, containView: View?, parent: ViewGroup): View? { var containView = containView val context = parent.context if (containView == null) { val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater containView = inflater.inflate(R.layout.viewwithdrawmonthly_item, parent, false) } val chkTy = containView?.findViewById<CheckBox>(R.id.chkTy) val monthlyDay = containView?.findViewById<TextView>(R.id.editMonthlyDay) val remark = containView?.findViewById<TextView>(R.id.editRemark) val outamt = containView?.findViewById<TextView>(R.id.editOutAmt2) val textSplit = containView?.findViewById<TextView>(R.id.textSplit) val textSplit1 = containView?.findViewById<TextView>(R.id.textSplit1) val viewWithDrawMonthly = viewWithDrawMonthlyls[position] if (chkTy != null) { chkTy.isChecked = false } if ("Y" == viewWithDrawMonthly.chkTy) { if (chkTy != null) { chkTy.isChecked = true } } if (monthlyDay != null) { monthlyDay.text = viewWithDrawMonthly.getmonthlyDay() } if (remark != null) { remark.text = viewWithDrawMonthly.remark } if (outamt != null) { outamt.text = viewWithDrawMonthly.outAmt } return containView } }
kotlin에서 non-null 이 null 될 수 없어서 오류가 발생하기 때문이기도 합니다.
처리하는 기술적인 부분에 대해서는 이 글이 참고가 되었습니다. 안드로이드가 External Storage에 대한 access 권한을 제한하기 전에 안드로이드 11 이전에는 READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 등으로 카메라 앱으로 촬영한 사진등의 이미지를 접근해 백업을 하거나 하는 처리를 할 수 있었습니다.
하지만, 안드로이드가 업데이트를 하는 과정에서 여러 가지 보안절차가 강화되었기 때문에 그걸 활용할 수 없었습니다.
manifest에 등록한 권한 목록입니다. notify을 하기 위한 권한, wifi 상태 확인을 위한 권한등은 기능설정을 위해서 필요한 부분이고,
READ_MEDIA_IMAGES 가 필수 권한입니다. READ_EXTERNAL_STOREAGE 은 안드로이드 API 32까지만 필요하므로 이제 지워도 될 것 같습니다. 이제 데이터를 읽어 오는 부분에 대한 코드를 작성해 보겠습니다.
withContext(Dispatchers.IO) {
/** * Android [ContentProvider]로 작업할 때 핵심 개념은 * "투영". 프로젝션은 공급자에게 요청할 열의 목록입니다. * (정확히) SQL의 "SELECT ..." 절로 생각할 수 있습니다. * 성명. * * 프로젝션을 제공하는 것은 _필수_가 아닙니다. 이 경우 `null`을 전달할 수 있습니다. * [ContentResolver.query]에 대한 호출에서 `projection` 대신 요청하지만 * 필요한 것보다 더 많은 데이터는 성능에 영향을 미칩니다. * * 이 샘플에서는 몇 개의 데이터 열만 사용하므로 * 열의 하위 집합. */ /** * Android [ContentProvider]로 작업할 때 핵심 개념은 * "투영". 프로젝션은 공급자에게 요청할 열의 목록입니다. * (정확히) SQL의 "SELECT ..." 절로 생각할 수 있습니다. * 성명. * * 프로젝션을 제공하는 것은 _필수_가 아닙니다. 이 경우 `null`을 전달할 수 있습니다. * [ContentResolver.query]에 대한 호출에서 `projection` 대신 요청하지만 * 필요한 것보다 더 많은 데이터는 성능에 영향을 미칩니다. * * 이 샘플에서는 몇 개의 데이터 열만 사용하므로 * 열의 하위 집합. */ val projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_ADDED, )
/** * `selection`은 SQL 문의 "WHERE ..." 절입니다. 가능하다 * 대신 `null`을 전달하여 이를 생략하면 모든 행이 반환됩니다. * 이 경우 이미지를 촬영한 날짜를 기준으로 선택 항목을 사용하고 있습니다. * * 선택 항목에 `?`가 포함되어 있습니다. 이것은 변수를 나타냅니다. * 다음 변수에 의해 제공됩니다. */ /** * `selection`은 SQL 문의 "WHERE ..." 절입니다. 가능하다 * 대신 `null`을 전달하여 이를 생략하면 모든 행이 반환됩니다. * 이 경우 이미지를 촬영한 날짜를 기준으로 선택 항목을 사용하고 있습니다. * * 선택 항목에 `?`가 포함되어 있습니다. 이것은 변수를 나타냅니다. * 다음 변수에 의해 제공됩니다. */ val selection = "${MediaStore.Images.Media.DATE_ADDED} >= ?"
/** * `selectionArgs`는 각 `?`에 대해 채워질 값 목록입니다. * `선택`에서. */ /** * `selectionArgs`는 각 `?`에 대해 채워질 값 목록입니다. * `선택`에서. */ val selectionArgs = arrayOf( dateToTimestamp(1, 1, 2000).toString() ) selectionArgs.forEach { Log.e("", "selectionArgs $it ${getDateTime(it)}") }
/** * Sort order to use. This can also be null, which will use the default sort * order. For [MediaStore.Images], the default sort order is ascending by date taken. */ /** * Sort order to use. This can also be null, which will use the default sort * order. For [MediaStore.Images], the default sort order is ascending by date taken. */ val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
/** * 반환된 [Cursor]에서 데이터를 가져오려면 * 관심 있는 각 열과 일치하는 인덱스를 찾습니다. * * 두 가지 방법이 있습니다. 첫 번째는 방법을 사용하는 것입니다 * 열 ID를 찾을 수 없는 경우 -1을 반환하는 [Cursor.getColumnIndex]. 이것 * 코드가 요청할 열을 프로그래밍 방식으로 선택하는 경우에 유용합니다. * 그러나 객체로 파싱하기 위해 단일 방법을 사용하고 싶습니다. * * 우리의 경우 원하는 열을 정확히 알고 있기 때문에 * 반드시 포함되어야 한다는 점(API 1에서 모두 지원되기 때문에) * [Cursor.getColumnIndexOrThrow]를 사용합니다. 이 방법은 * [IllegalArgumentException] 명명된 열을 찾을 수 없는 경우. * * 두 경우 모두 이 방법이 느리지는 않지만 결과를 캐시하려고 합니다. * 각 행에 대해 조회할 필요가 없도록 합니다. */ /** * 반환된 [Cursor]에서 데이터를 가져오려면 * 관심 있는 각 열과 일치하는 인덱스를 찾습니다. * * 두 가지 방법이 있습니다. 첫 번째는 방법을 사용하는 것입니다 * 열 ID를 찾을 수 없는 경우 -1을 반환하는 [Cursor.getColumnIndex]. 이것 * 코드가 요청할 열을 프로그래밍 방식으로 선택하는 경우에 유용합니다. * 그러나 객체로 파싱하기 위해 단일 방법을 사용하고 싶습니다. * * 우리의 경우 원하는 열을 정확히 알고 있기 때문에 * 반드시 포함되어야 한다는 점(API 1에서 모두 지원되기 때문에) * [Cursor.getColumnIndexOrThrow]를 사용합니다. 이 방법은 * [IllegalArgumentException] 명명된 열을 찾을 수 없는 경우. * * 두 경우 모두 이 방법이 느리지는 않지만 결과를 캐시하려고 합니다. * 각 행에 대해 조회할 필요가 없도록 합니다. */ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED) val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
while (cursor.moveToNext()) {
// Here we'll use the column indexs that we found above. val id = cursor.getLong(idColumn) val dateModified = Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn))) val displayName = cursor.getString(displayNameColumn)
/** * 이것은 가장 까다로운 부분 중 하나입니다. * * 이미지에 액세스하고 있기 때문에(사용하여 * [MediaStore.Images.Media.EXTERNAL_CONTENT_URI], 이를 사용하겠습니다. * 기본 URI로 이미지의 ID를 추가합니다. * * 이것은 [MediaStore.Video]로 작업할 때와 완전히 동일한 방법이며 * [MediaStore.Audio]도 마찬가지입니다. `Media.EXTERNAL_CONTENT_URI`가 무엇이든 * 아이템을 얻기 위한 쿼리가 기본이고, ID는 받을 문서입니다. * 거기에 요청하십시오. */
/** * 이것은 가장 까다로운 부분 중 하나입니다. * * 이미지에 액세스하고 있기 때문에(사용하여 * [MediaStore.Images.Media.EXTERNAL_CONTENT_URI], 이를 사용하겠습니다. * 기본 URI로 이미지의 ID를 추가합니다. * * 이것은 [MediaStore.Video]로 작업할 때와 완전히 동일한 방법이며 * [MediaStore.Audio]도 마찬가지입니다. `Media.EXTERNAL_CONTENT_URI`가 무엇이든 * 아이템을 얻기 위한 쿼리가 기본이고, ID는 받을 문서입니다. * 거기에 요청하십시오. */
val contentUri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id )
val image = MediaStoreImage(id, displayName, dateModified, contentUri) images += image
// For debugging, we'll output the image objects we create to logcat. // Log.e("", "Added image: $displayName $dateModified ${TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn))}") } }
이 코드는 android 개발자 페이지의 open 된 코드들 중에서 storeage-samples 안에 있는 MediaStore 프로젝트에서 가지고 왔음을 밝혀 둡니다.
#스하리1000명프로젝트, Terkadang sulit untuk berbicara dengan pekerja asing, bukan? Saya membuat aplikasi sederhana yang membantu! Anda menulis dalam bahasa Anda, dan orang lain melihatnya dalam bahasa mereka. Ini menerjemahkan secara otomatis berdasarkan pengaturan. Sangat berguna untuk obrolan mudah. Lihatlah ketika Anda mendapat kesempatan! https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416
#billcorea #운동동아리관리앱 🏸 Schneedle, aplikasi yang wajib dimiliki oleh klub bulu tangkis! 👉 Match Play – Rekam Skor & Temukan Lawan 🎉 Sempurna untuk di mana saja, sendirian, bersama teman, atau di klub! 🤝 Jika Anda suka bulu tangkis, cobalah