1. 메인 content layout과 menu 리소스를 생성자에서 지정하기
class MainActivity : BaseAppCompatActivity(R.layout.activity_main, R.menu.menu_main) {
2. View와 id의 바인딩을 한 줄로 해결하기, intent 값을 한 줄로 초기화 하기
private val testView: TextView by findView(R.id.test)
private val name: String by intentString("name")
private val age: Int by intentInt("age")
private val hasChild: Boolean by intentBoolean("hasChild")
private val count: Long by intentLong("count")
3. 버튼 핸들러와 메뉴 핸들러를 annotation만으로 간단히 처리하기
@OnMenuClick(R.id.action_settings)
fun onSettings(menuItem: MenuItem) {
showToast("age: $age")
}
@OnClick(R.id.test)
fun onClickTest(v: View) {
showToast("name: $name")
}
4. startActivityForResult를 requestCode없이 사용하기
startActivityForResult(
Intent().setType("image/*").setAction(Intent.ACTION_GET_CONTENT)
) { resultCode, data ->
showToast("reuslt1=$resultCode\ndata= ${data?.data}")
}
5. permisson 요청을 requestCode없이 사용하기
checkPermissions(
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
) { isAllGranted, shouldShowRational ->
if (isAllGranted) showToast("Granted")
else showToast("Denied $shouldShowRational")
}
6. BaseAppCompatActivity의 전체 소스
package com.mdiwebma.base
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.lang.reflect.Method
typealias ActionListener = () -> Unit
typealias OnActivityResultAction = (resultCode: Int, data: Intent?) -> Unit
// shouldShowRational: 최소 1개 거부하면 true, 모두 다시 안 보기 하면 false
typealias OnPermissionResultAction = (isAllGranted: Boolean, shouldShowRational: Boolean) -> Unit
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class OnMenuClick(val menuId: Int)
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class OnClick(val viewId: Int)
fun <ViewType : View> Activity.findView(@IdRes viewId: Int): Lazy<ViewType> =
lazy(LazyThreadSafetyMode.NONE) { findViewById<ViewType>(viewId) }
fun <ViewType : View> View.findView(@IdRes viewId: Int): Lazy<ViewType> =
lazy(LazyThreadSafetyMode.NONE) { findViewById<ViewType>(viewId) }
fun Activity.intentString(key: String, fallback: String = ""): Lazy<String> =
lazy(LazyThreadSafetyMode.NONE) {
if (intent.hasExtra(key)) intent.getStringExtra(key).orEmpty() else fallback
}
fun Activity.intentNullableString(key: String, fallback: String? = null): Lazy<String?> =
lazy(LazyThreadSafetyMode.NONE) {
if (intent.hasExtra(key)) intent.getStringExtra(key) else fallback
}
fun Activity.intentBoolean(key: String, fallback: Boolean = false): Lazy<Boolean> =
lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(key, fallback) }
fun Activity.intentInt(key: String, fallback: Int = 0): Lazy<Int> =
lazy(LazyThreadSafetyMode.NONE) { intent.getIntExtra(key, fallback) }
fun Activity.intentLong(key: String, fallback: Long = 0L): Lazy<Long> =
lazy(LazyThreadSafetyMode.NONE) { intent.getLongExtra(key, fallback) }
fun Activity.intentDouble(key: String, fallback: Double = 0.0): Lazy<Double> =
lazy(LazyThreadSafetyMode.NONE) { intent.getDoubleExtra(key, fallback) }
fun Context.showToast(@StringRes stringId: Int, isLong: Boolean = false) {
showToast(getString(stringId), isLong)
}
fun Context.showToast(message: String, isLong: Boolean = false) {
Toast.makeText(this, message, if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show()
}
fun Any.handleOnClickAnnotation(view: View) {
this.javaClass.declaredMethods.forEach { method ->
val onClick = method.getAnnotation(OnClick::class.java) ?: return@forEach
view.findViewById<View>(onClick.viewId)?.setOnClickListener { v ->
method.invoke(this, v)
}
}
}
abstract class BaseAppCompatActivity(
@LayoutRes private val contentLayoutId: Int,
@MenuRes private val optionsMenuId: Int = 0
) : BaseActivity(contentLayoutId) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view: View = findViewById(android.R.id.content)
if (contentLayoutId != 0) handleOnClickAnnotation(view)
onViewCreated(view, savedInstanceState)
}
open fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// empty
}
/**
* Options menus helper
*/
protected lateinit var optionsMenu: Menu
private val optionsMenuHandlerMap: HashMap<Int, Method> by lazy {
HashMap<Int, Method>().also { map ->
this.javaClass.declaredMethods.forEach { method ->
val onMenuClick = method.getAnnotation(OnMenuClick::class.java) ?: return@forEach
map[onMenuClick.menuId] = method
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
optionsMenu = menu
if (optionsMenuId != 0) {
menuInflater.inflate(optionsMenuId, menu)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val method = optionsMenuHandlerMap[item.itemId]
when {
method != null -> method.invoke(this, item)
item.itemId == android.R.id.home -> finish()
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* OnActivityResult Helper
*/
private val onActivityResultActionMap = HashMap<Int, OnActivityResultAction>()
fun startActivityForResult(intent: Intent, onActivityResultAction: OnActivityResultAction) {
val requestCode = makeValidRequestCode(onActivityResultAction)
onActivityResultActionMap[requestCode] = onActivityResultAction
super.startActivityForResult(intent, requestCode)
}
private fun makeValidRequestCode(action: Any) = action.hashCode() and 0x0000ffff
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
onActivityResultActionMap[requestCode]?.let {
it.invoke(resultCode, data)
onActivityResultActionMap.remove(requestCode)
} ?: super.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroy() {
onActivityResultActionMap.clear()
permissionRequestMap.clear()
super.onDestroy()
}
/**
* Permission Helper
*/
private val permissionRequestMap = HashMap<Int, OnPermissionResultAction>()
fun checkPermissions(
permissions: Array<String>,
onPermissionResultAction: OnPermissionResultAction
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
permissions.all {
ContextCompat.checkSelfPermission(
this, it
) == PackageManager.PERMISSION_GRANTED
}
) {
onPermissionResultAction.invoke(true, false)
return
}
val requestCode = makeValidRequestCode(onPermissionResultAction)
permissionRequestMap[requestCode] = onPermissionResultAction
ActivityCompat.requestPermissions(this, permissions, requestCode)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
permissionRequestMap[requestCode]?.let { resultAction ->
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
resultAction.invoke(true, false)
} else {
val shouldShowRational = permissions.any {
ActivityCompat.shouldShowRequestPermissionRationale(this, it)
}
resultAction.invoke(false, shouldShowRational)
}
permissionRequestMap.remove(requestCode)
} ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
7. BaseFragment의 전체 소스
package com.mdiwebma.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
fun Fragment.argumentString(key: String, fallback: String = ""): Lazy<String> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getString(key, fallback).orEmpty() }
fun Fragment.argumentNullableString(key: String, fallback: String? = null): Lazy<String?> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getString(key, fallback) }
fun Fragment.argumentBoolean(key: String, fallback: Boolean = false): Lazy<Boolean> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getBoolean(key, fallback) }
fun Fragment.argumentInt(key: String, fallback: Int = 0): Lazy<Int> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getInt(key, fallback) }
fun Fragment.argumentLong(key: String, fallback: Long = 0L): Lazy<Long> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getLong(key, fallback) }
fun Fragment.argumentDouble(key: String, fallback: Double = 0.0): Lazy<Double> =
lazy(LazyThreadSafetyMode.NONE) { requireArguments().getDouble(key, fallback) }
fun <ViewType : View> Fragment.findView(@IdRes viewId: Int): Lazy<ViewType> =
lazy(LazyThreadSafetyMode.NONE) { requireView().findViewById<ViewType>(viewId) }
abstract class BaseFragment(@LayoutRes private val contentLayoutId: Int) : Fragment() {
protected var isVisibleToUser: Boolean = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = LayoutInflater.from(context).inflate(contentLayoutId, null).also {
handleOnClickAnnotation(it)
}
override fun setMenuVisibility(menuVisible: Boolean) {
super.setMenuVisibility(menuVisible)
if (isVisibleToUser != menuVisible) {
isVisibleToUser = menuVisible
onVisibleToUserChanged(menuVisible)
}
}
open fun onVisibleToUserChanged(isVisibleToUser: Boolean) {
// empty
}
}
8. Proguard rule에 annotaion예외 추가
-keep class com.mdiwebma.base.OnClick
-keep class com.mdiwebma.base.OnMenuClick
-keepclassmembers class * {
@com.mdiwebma.base.OnClick *;
@com.mdiwebma.base.OnMenuClick *;
}