Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

안드로이드 앱 만들기 : java 프로젝트에서 kotlin 으로 넘어가 보기

Billcorea 2023. 7. 24. 18:08
반응형

이 글은 기존에 사용하던 java 프로젝트를 kotlin으로 전환해 보기 위해서 작성하고 있습니다. 이 글은 아래 링크의 개발자 페이지의 내용을 참고하고 있습니다.

 

https://developer.android.com/kotlin/add-kotlin?hl=ko#kts 

 

기존 앱에 Kotlin 추가  |  Android Developers

기존 앱에 Kotlin 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 스튜디오에서는 Kotlin을 완벽하게 지원하므로 기존 프로젝트에 Kotlin 파일을 추가

developer.android.com

 

먼저 gradle 파일부터 수정해 보겠습니다. 

 

buildscript {

    ext { // 추가한 부분
        compose_ui_version = '1.5.0-alpha04'
        raamcosta_version = '1.9.42-beta'
        kotlin_version = '1.8.22' 
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.0.2'
        classpath 'com.google.gms:google-services:4.3.15'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // 추가한 부분
    }
}

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

다음 모듈 수준의 gradle 파일 수정입니다.

import java.text.SimpleDateFormat

plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
    id 'kotlin-android' // 추가한 부분
}

android {
    signingConfigs {
        upload {
            storeFile file(var)
            storePassword var
            keyAlias = var
            keyPassword var
        }
    }
    compileSdkVersion 34
    namespace='com.n.......y2kakao'

    defaultConfig {
        applicationId "com.n......akao"
        minSdkVersion 26
        targetSdkVersion 34
        versionCode 41
        versionName '1.4.1'
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        renderscriptTargetApi 18
        renderscriptSupportModeEnabled true

    }

    sourceSets {  // 추가한 부분
        getByName("main") {
            java.srcDir("src/main/kotlin")
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    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.google.android.play:core:1.10.3"
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.preference:preference-ktx:1.2.0'
    implementation 'com.google.gms:google-services:4.3.15'
    implementation platform('com.google.firebase:firebase-bom:30.1.0')
    implementation 'com.google.firebase:firebase-crashlytics'
    implementation 'com.google.firebase:firebase-analytics'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
    implementation 'com.google.android.gms:play-services-ads:22.2.0'
    implementation 'com.google.android.gms:play-services-maps:18.1.0'
    implementation 'androidx.mediarouter:mediarouter:1.4.0'
    implementation 'com.google.guava:guava:29.0-android'

    // 데이터 주고 받기
    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 될 수 없어서 오류가 발생하기 때문이기도 합니다.

 

다음이야기는 뒤에...

반응형