Seize the day

POST : Android Dev Study

200줄짜리 BaseAppCompatActivity로 귀찮은 일 줄이기

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 *;
}

 

 

 
top

posted at

2020. 3. 15. 00:00


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크