์ธ์ฑ ์ ๋ฐ์ดํธ๋ ์ดํ ๋ด์์ ์ฌ์ฉ์์๊ฒ ์ฑ์ ์ ๋ฐ์ดํธํ๋ผ๊ณ ๋ฉ์์ง๋ฅผ ํ์ํ๋ Google Play Core ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ฅ์ ๋๋ค.
์ด๋ Developers์ ๋ฌธ์ฅ์ด๊ณ , ์กฐ๊ธ ๋ ์ฝ๊ฒ ๋งํ์๋ฉด ์ดํ ๋ด์์ ์ ๋ฐ์ดํธ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์ฒดํฌํ๊ณ , ๋ฐ๋ก ์ ๋ฐ์ดํธ๋ฅผ ์งํํ ์ ์๋ ๊ธฐ๋ฅ์ด๋ผ๊ณ ํ ์ ์์ต๋๋ค.
์ธ์ฑ ์ ๋ฐ์ดํธ์๋ ๋ ๊ฐ์ง ํ๋ฆ์ผ๋ก ์งํ๋ฉ๋๋ค.
์ ์ฐํ ์ ๋ฐ์ดํธ (Flexible Update)
์ฆ์ ์ ๋ฐ์ดํธ (Immediate Update)
Flexible Update๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ๋ค์ด๋ก๋๋ก ์งํ๋์ด ์ ๋ฐ์ดํธ๊ฐ ์ค์น๋๋ ๋์ ์ฌ์ฉ์๊ฐ ์ฑ์ ์ฌ์ฉํ ์ ์๋ ์ ๋ฐ์ดํธ ๋ฐฉ์์ ๋๋ค.
Immediate Update๋ ํฌ๊ทธ๋ผ์ด๋์์ ๋ค์ด๋ก๋๊ฐ ์งํ๋๊ธฐ ๋๋ฌธ์, ์ ๋ฐ์ดํธ๋ฅผ ์คํํ๋ฉด ์ฌ์ฉ์๋ ์ ๋ฐ์ดํธ์ ์ค์น์ ์ฌ์คํ๋๊ธฐ๊น์ง์ ๊ณผ์ ์ ๊ธฐ๋ค๋ฆฐ ํ์ ์ฑ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ธ์ฑ ์ ๋ฐ์ดํธ๋ ์ ๋ฐ์ดํธ ์ฌ๋ถ๋ฅผ ๋ฌป๋ ํ์ ์ฐฝ๊ณผ ์ค์น progress ํ๋ฉด ๋ชจ๋ Google Play Core์์ ๋ณด์ฌ์ฃผ๋ ํ๋ฉด์ด ๋์์ง๋๋ค.
https://developer.android.com/guide/playcore/in-app-updates/kotlin-java?hl=ko#kts
์ธ์ฑ ์ ๋ฐ์ดํธ ์ง์(Kotlin ๋๋ ์๋ฐ) | Android ๊ฐ๋ฐ์ | Android Developers
์ธ์ฑ ์ ๋ฐ์ดํธ ์ง์(Kotlin ๋๋ ์๋ฐ) ์ปฌ๋ ์ ์ ์ฌ์ฉํด ์ ๋ฆฌํ๊ธฐ ๋ด ํ๊ฒฝ์ค์ ์ ๊ธฐ์ค์ผ๋ก ์ฝํ ์ธ ๋ฅผ ์ ์ฅํ๊ณ ๋ถ๋ฅํ์ธ์. ์ด ๊ฐ์ด๋์์๋ Kotlin์ด๋ ์๋ฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ฑ์์ ์ธ์ฑ ์ ๋ฐ์ดํธ๋ฅผ ์ง
developer.android.com
1. ๊ฐ๋ฐ ํ๊ฒฝ ์ค์
// In your app’s build.gradle.kts file:
dependencies {
implementation("com.google.android.play:app-update:2.0.0")
implementation("com.google.android.play:app-update-ktx:2.0.0")
}
2. ์ ๋ฐ์ดํธ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
val appUpdateManager = AppUpdateManagerFactory.create(context)
val appUpdateInfo = appUpdateManager.appUpdateInfo
appUpdateInfo.addOnSuccessListener { _appUpdateInfo ->
if (_appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& _appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// TODO ์
๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํ ๋
}
}
์ ์ฝ๋๋ ํ์ฌ ์ค์น๋์ด ์๋ ์ฑ์ด ํ๋ ์ด์คํ ์ด์ ์ฌ๋ผ๊ฐ ์ฑ๊ณผ ๋น๊ตํด์ ์ ๋ฐ์ดํธ๊ฐ ํ์ํ์ง ์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์๋ ์ฝ๋์ ๋๋ค.
์๋ ์กฐ๊ฑด๋ฌธ์์ appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)์๋ IMMEDIATE๋์ FLEXIBLE์ ๋ฃ์ด์ค ์ ์์ต๋๋ค.
์ ๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํ ๋ ์ฝ๋๋ก๋ ๋ฐ๋ก ์ ๋ฐ์ดํธ๋ฅผ ์คํํด์ฃผ๊ฑฐ๋, ์ ๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํ๋ค๋ UI๋ฅผ ๋ณ๊ฒฝ์์ผ์ฃผ๋ ์ฝ๋๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
3. ์ ๋ฐ์ดํธ ์์
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE, // AppUpdateType.FLEXIBLE
this, //activity
MY_REQUEST_CODE // ํด๋น ์
๋ฐ์ดํธ ์์ฒญ์ ๋ชจ๋ํฐํ ์ ์๋ ์์ request code
)
์ ์ผ ์ค์ํ ๊ฒ ๊ฐ AppUpdateInfo ๊ฐ์ผ๋ก๋ ์ ๋ฐ์ดํธ๋ฅผ ํ ๋ฒ๋ง ์์ํ ์ ์์ต๋๋ค.
AppUpdateManager์ AppUpdateInfo๊ฐ์ ์ ์ญ์ ์ผ๋ก ์ ์ฅํด ๋๊ณ ์ฌ์ฉํ๋ฉด ํธํ์ง ์์๊น ํ๋๋ฐ
AppUpdateInfo ๊ฐ์ด ์์ฒญํด์ ๋ฐ์ ๋๋ง๋ค ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์
์ ๋ฐ์ดํธ๋ฅผ ์คํจํ ๊ฒฝ์ฐ ์ ๋ฐ์ดํธ๋ฅผ ๋ค์ ์๋ํ๋ ค๋ฉด ์ AppUpdateInfo๋ฅผ ์์ฒญํด์ ์ ๋ฐ์ดํธ ์์ฒญ์ ๋ค์ ๋ณด๋ด์ผ ํฉ๋๋ค.
4. ์ ๋ฐ์ดํธ ์ํ์ ๊ดํ ์ฝ๋ฐฑ ๊ฐ์ ธ์ค๊ธฐ
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MY_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
/*
RESULT_CANCELED
์
๋ฐ์ดํธ๋ฅผ ๊ฑฐ๋ถํ๊ฑฐ๋ ์ทจ์ํ์ ๋
ActivityResult.RESULT_IN_APP_UPDATE_FAILED
๊ธฐํ ์ค๋ฅ๋ก ์ธํด ์ฌ์ฉ์๊ฐ ๋์ํ์ง ๋ชปํ๊ฑฐ๋ ์
๋ฐ์ดํธ๊ฐ ์งํ๋์ง ๋ชปํ์ ๋
*/
}
}
}
์ ๋ฐ์ดํธ๊ฐ ์ทจ์๋์์ ๋๋ ํ ์คํธ ๋ฉ์์ง๋ฅผ ๋์์ฃผ๊ฑฐ๋ AppUpdateInfo ๊ฐ์ ์๋ก ๋ฐ์์ค๋๋ก ํ ์ ์์ต๋๋ค.
์ ๋ฐ์ดํธ๊ฐ ์ฑ๊ณตํ์ ๋์ ์ฒ๋ฆฌ๋ ๋ณ๋๋ก ์์ฑ๋๋ ๊ฑฐ๋ถํ๊ฑฐ๋ ์ทจ์ํ์ ๋, ํน์ ๊ธฐํ ์ค๋ฅ๋ก ์งํ๋์ง ์์ ๋์ ์ฒ๋ฆฌ๋ง ์์ฑํด์ฃผ๋ฉด ๋ฉ๋๋ค.
5-1. Flexible Update ์ฒ๋ฆฌ
// ์ค์น ์ํ ๋ชจ๋ํฐ๋ง ๋ฆฌ์ค๋
val listener = InstallStateUpdatedListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
// TODO ์
๋ฐ์ดํธ๊ฐ ๋ค์ด๋ก๋ ์๋ฃ๋์์ ๋ ์ฒ๋ฆฌ
popupSnackbarForCompleteUpdate()
} else if (state.installStatus() == InstallStatus.INSTALLED) {
// TODO ์
๋ฐ์ดํธ๊ฐ ์ค์น ์๋ฃ๋์์ ๋ ์ฒ๋ฆฌ
}
}
// Displays the snackbar notification and call to action.
fun popupSnackbarForCompleteUpdate() {
Snackbar.make(
findViewById(R.id.activity_main_layout),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE
).apply {
setAction("RESTART") { appUpdateManager.completeUpdate() }
setActionTextColor(resources.getColor(R.color.snackbar_action_text_color))
show()
}
}
// ์
๋ฐ์ดํธ๋ฅผ ์์ํ๊ธฐ ์ ์ appUpdateManager์ Listener ๋ฑ๋ก
appUpdateManager.registerListener(listener)
// ์
๋ฐ์ดํธ ์์
// ์ํ ์
๋ฐ์ดํธ๊ฐ ๋์ด์ ๋ถํ์ํ ๋ appUpdateManager์ listener ๋ฑ๋ก ํด์
appUpdateManager.unregisterListener(listener)
์ ๋ฐ์ดํธ๊ฐ ์๋ฃ๋์์ ๋์ ์ฒ๋ฆฌ์ ๋๋ค.
์ ์ฝ๋๋ ์ค๋ต๋ฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ์ฝ๋๋ก ๋์ด์์ง๋ง, ๋ค์ด์ผ๋ก๊ทธ๋ฅผ ๋์์ค ์๋ ์๊ณ ์์ ๋กญ๊ฒ ๋ฐ๊ฟ์ค ์ ์๋๋ฐ,
appUpdateManager.completeUpdate()๋ง ์คํ์์ผ์ฃผ๋ฉด ๋ฉ๋๋ค.
๊ฐ์ธ์ ์ผ๋ก ํ ์คํธ ํด๋ณด์์ ๋๋ ์๋ ์ฝ๋๊ฐ ์ ์๋์ ํ์ง ์์์ต๋๋ค...
if (state.installStatus() == InstallStatus.DOWNLOADED) {
// TODO ์
๋ฐ์ดํธ๊ฐ ๋ค์ด๋ก๋ ์๋ฃ๋์์ ๋ ์ฒ๋ฆฌ
appUpdateManager.completeUpdate()
}
์ด์ ๋ฅผ ์์๋ ๋ถ์ ๋๊ธ ์ข
๋๋ ์ด ์ด์์ธ์ง ํ๋ฉด ๊ทธ๋ฆฌ๋ ๋ถ๋ถ์ ์ด์์ธ์ง ์ ๋ชจ๋ฅด๊ฒ ์ง๋ง
๋ค์ด์ผ๋ก๊ทธ๋ ์ค๋ต๋ฐ๊ฐ์ด ์ค๊ฐ ๋จ๊ณ๋ฅผ ํ๋ฒ ๊ฑฐ์น๋ฉด ์ ์ ๋์ํ๋ ๊ฑธ ํ์ธํ ์ ์์ต๋๋ค.
5-2. Immediate Update ์ฒ๋ฆฌ
override fun onResume() {
super.onResume()
appUpdateManager
.appUpdateInfo
.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability()
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
IMMEDIATE,
this,
MY_REQUEST_CODE
)
}
}
}
6. ์ ๋ฐ์ดํธ ํ ์คํธ
์ธ์ฑ ์ ๋ฐ์ดํธ ์ฝ๋ ์์ฑ ํ ํ ์คํธ๋ฅผ ํด๋ณด๊ธฐ ์ํด์๋ ์ธ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
1) ํ๋ ์ด ์ฝ์ - ๋ด๋ถ ํ ์คํธ
2) FakeAppUpdate
3) ํ๋ ์ด์คํ ์ด์ ์ถ์๋ ์ฑ๋ณด๋ค ๋ฒ์ ์ด ๋ฎ์ ๋ฆด๋ฆฌ์ฆ apk๋ฅผ ์ถ์ถ
์ ์ฒด ์ฝ๋
์ ๋ฐ์ดํธ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ ๊ณผ ๋์์ ์ฆ์ ์ ๋ฐ์ดํธ๋ฅผ ์คํํ๊ณ ์ ํ๋ค๋ฉด, ์ฑ์ด ์คํ๋๊ณ ๋ฐ๋ก ์ฒซ ํ๋ฉด์ ๋ ธ์ถ๋๋ ๊ฒ ์ข์ต๋๋ค.
๊ทธ๋ ์ง๋ง SplashScreen์ด๋ IntroActivity๊ฐ์ด ์งง๊ฒ ๋์ด๊ฐ๋ ํ๋ฉด์๋ ๋ถ์ ํฉํฉ๋๋ค.
Splash๊ฐ ๋์์ง๊ณ ์ ๋ฐ์ดํธ ํ์ ์ด ๋์์ก๋๋ฐ MainActivity๋ก ๋์ด๊ฐ๋ฉด์ ์ ๋ฐ์ดํธ ํ์ ์ด ์ฌ๋ผ์ง ์ ์๊ธฐ ๋๋ฌธ์
์ผ๋ฐ์ ์ธ MainActivity์ ๊ฐ์ด ์งง๊ฒ ๋์ด๊ฐ๋ ํ๋ฉด ๊ทธ ์ดํ์ ๊ฐ์ฅ ์ฒซ ํ๋ฉด์์ ๋ ธ์ถ๋๋ ๊ฒ ์ข์ต๋๋ค.
//MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var appUpdateManager: AppUpdateManager
private val REQUEST_CODE = 42
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
checkForUpdates()
}
override fun onResume() {
super.onResume()
appUpdateManager.appUpdateInfo.addOnSuccessListener { _appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this@MainActivity,
REQUEST_CODE
)
}
}
private val stateUpdatedListener: InstallStateUpdatedListener =
InstallStateUpdatedListener { state ->
state?.let {
if (it.installStatus == InstallStatus.DOWNLOADED) {
Snackbar.make(
findViewById(R.id.container),
"์
๋ฐ์ดํธ๊ฐ ์๋ฃ๋์์ต๋๋ค.",
Snackbar.LENGTH_INDEFINITE
).apply {
setAction("์ฌ์คํํ๊ธฐ") { appUpdateManager.completeUpdate() }
show()
}
}
}
}
}
private fun checkForUpdates() {
appUpdateManager = AppUpdateManagerFactory.create(applicationContext)
appUpdateManager.appUpdateInfo.addOnSuccessListener { _appUpdateIntent ->
if (_appUpdateIntent.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& appUpdateIntent.isUpdateTypeAllowed(IMMEDIATE)) {
appUpdateManager.registerListener(stateUpdatedListener)
appUpdateManager.startUpdateFlowForResult(
_appUpdateIntent,
IMMEDIATE,
this,
REQUEST_CODE
)
}
}
}
override fun onDestroy() {
super.onDestroy()
appUpdateManager.unregisterListener(stateUpdatedListener)
}
}
์ ๋ฐ์ดํธ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ ์ ์ฑ์ด ์คํ๋๊ณ ๋ฐ๋กํ๋, ์ ๋ฐ์ดํธ ํ์ ์ ๋ค๋ฅธ ํ๋ฉด์์ ๋ ธ์ถํ๊ณ ์ถ๋ค๋ฉด
AppUpdateManager์ AppUpdateInfo๋ฅผ MainActivity์ ์ ์ฅํด๋๊ณ
๋ค๋ฅธ ํ๋ฉด์์ ์ ๋ฐ์ดํธ ์คํ ์ฝ๋๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋ฉ๋๋ค.
AppUpdateManager๋ฅผ MainActivity์ ๋ฃ์ด์ฃผ๋ ์ด์ ๋ ๊ผญ MainActivity์์ ํด์ผํ๋ค๋ ๊ฒ ์๋๊ณ
์ ๋ฐ์ดํธ๊ฐ ์๋ฃ๋๊ณ ์ฑ์ด ์ฌ์คํ๋์ด์ผ ํ ๋
์ ๋ฐ์ดํธ๊ฐ ์๋ฃ๋์๋ค๋ ๊ฑธ ๊ฐ์ฅ Base๊ฐ ๋๋ ํ๋ฉด์์ ์๊ณ ์์ด์ผ ๋ค๋ฅธ ํ๋ฉด์ ์๋ค๊ฐ๋ ์ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๊ทธ ์ด์ธ์ ์๋ชป๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ๊ผญ ๊ผญ ๊ผญ ๋๊ธ๋ก ์๋ ค์ฃผ์๊ธธ ๋ฐ๋๋๋ค.
๊ฐ์ฌํฉ๋๋ค.