기존에 startActivityForResult() 및 onActivityResult() 를 통해서 결과를 받았던 것을 Activity Result API는 시스템에서 전달되면 결과를 등록, 실행 및 처리하기 위한 구성요소를 제공합니다.

 

참고 URL

https://developer.android.com/training/basics/intents/result?hl=ko

 

활동으로부터 결과 가져오기  |  Android 개발자  |  Android Developers

개발자 앱 내의 활동이든 다른 앱의 활동이든 다른 활동을 시작하는 것이 단방향 작업일 필요는 없습니다. 다른 활동을 시작하고 다시 결과를 받을 수도 있습니다. 예를 들어 앱에서 카메라 앱

developer.android.com

사용해보기 

 

1. gradle 설정 

implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'

2. ActivityResultLauncher 생성 

@RequiresApi(Build.VERSION_CODES.M)
private val singlePermissions = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) {  isSuccess ->
    Log.d(TAG, "isSuccess : " + isSuccess)
}

registerForActivityResult 파라미터로 ActivityResultContracts, 과 ActivityResultCallback를 넘기게 됩니다. 

 

3. 실행 

singlePermissions.launch(permissionName)

 

4. 2번의 결과 로그 확인 

정상적으로 동작하는것을 확인해 보았습니다. 

 

동작확인

@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultCallback<O> callback) {
    return registerForActivityResult(contract, mActivityResultRegistry, callback);

1. 위 registerForActivityResult를 통해서  ActivityResultLauncher를 생성한 인스턴스를 실행시키는 코드로 ActivityResultLauncher를 생성합니다.

2. 생성시 첫번재 파라미터로 전달된 RequestPermission는 권한요청을 위해  ActivityResultContract를 상속받아 구현되어 제공되고있는 클래스로 하기와 같이 구현되어 있습니다.

  간단히 보면 createIntent메소드에 permission을 전달받고 parseResult로 결과를 제공합니다.

 

 

3. 하기 FragmentActivity에서onRequestPermissionsResult 오버라이딩메소드에서 조건 검사를 통해서 등록이 되어있으면 doDispatch의 callback.onActivityResult(contract.parseResult(resultCode, data))를 통해서 전달이 되어 두번째 파라미터로 전달된 callback메소드로 결과를 받을 수 있습니다.

@CallSuper
    @Override
    @Deprecated
    public void onRequestPermissionsResult(
            int requestCode,
            @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        if (!mActivityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, new Intent()
                .putExtra(EXTRA_PERMISSIONS, permissions)
                .putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))) {
            if (Build.VERSION.SDK_INT >= 23) {
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            }
        }
    }


private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
        @Nullable CallbackAndContract<O> callbackAndContract) {
    if (callbackAndContract != null && callbackAndContract.mCallback != null) {
        ActivityResultCallback<O> callback = callbackAndContract.mCallback;
        ActivityResultContract<?, O> contract = callbackAndContract.mContract;
        callback.onActivityResult(contract.parseResult(resultCode, data));
    } else {
        mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
    }
}

 

간단히 사용해보았는데 기존에 onActivityResult를 오버라이딩해서 처리했던것을 제공된 api를 사용하여 하기 구글 예제와 같이 ActivityResultContract를 상속받아 구현을 하면 동작에 대한 요청과 결과의 소스관리과 명확해 질 것 같습니다.

class PickRingtone : ActivityResultContract<Int, Uri?>() {
        override fun createIntent(context: Context, ringtoneType: Int) =
            Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
                putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
            }

        override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
            if (resultCode != Activity.RESULT_OK) {
                return null
            }
            return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
        }
    }

 

기존에 SharedPreferences에서 저장시 plain text인 것을  EncryptedSharedPreferences를 이용하면 암호화되어 저장하는 기능을 제공합니다. 

 

자세한 내용은 

https://developer.android.com/topic/security/data?hl=ko

 

Android 개발자  |  Android Developers

더 안전하게 데이터 사용 Android Jetpack의 일부 보안 라이브러리는 저장 데이터 읽기 및 쓰기와 관련된 보안 권장사항의 구현과 키 생성 및 인증을 제공합니다. 라이브러리는 빌더 패턴을 사용하��

developer.android.com

Android 6.0(API 수준 23) 이상을 실행하는 기기에서 지원됩니다

 

 

App내에 ID, PASSWORD와 같은 중요정보를 AES 대칭키(암호화, 복호화 키 동일)를 이용하여  암호화를 했을때 문제점은 

키 관리였습니다.

프로젝트내에 하드코딩된 문자열은 보안상 취약하므로 노출의 위험이 있습니다.

 

EncryptedSharedPreferences를 이용하여 저장해 보도록 하겠습니다.

라이브러리 내부적으로 KeyStore를 이용하여 키 접근은 application레벨에서 접근이 안되어 안전하다고 볼 수 있습니다. 

https://developer.android.com/training/articles/keystore?hl=ko

 

Android Keystore 시스템  |  Android 개발자  |  Android Developers

Android Keystore 시스템을 사용하면 암호화 키를 컨테이너에 저장하여 기기에서 키를 추출하기 어렵게 할 수 있습니다. 키 저장소에 키가 저장되면, 키 자료는 내보낼 수 없는 상태로 유지하면서 키

developer.android.com

gradle

implementation "androidx.security:security-crypto:1.0.0-rc03"

 

테스트 코드

val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

val SharedPreferences = EncryptedSharedPreferences
        .create(
            "android",
            masterKeyAlias,
            requireContext(),
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )

val sharedPrefsEditor = SharedPreferences.edit()
sharedPrefsEditor?.apply {
    putString("id", "test")
    putString("password", "test")
}
sharedPrefsEditor.apply()

Log.d(TAG, SharedPreferences.getString("id", "not found").toString())

 

확인 XML 열어보기

/data/data/[packageName]/shared_prefs/android.xml 

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a90115c36d71e8bd3b39a8409886b09616e196b34e6df40b5a585f7223e34216ff542cb59dcd2759720826c95dad26a36aa7ce081d9e4eeddc9bc1aee8f1fb2cb473bf3d2acfb459e12cb760661372c8322e682cc7b5982ef9fccd6a9699643d75eca2b63adbaf166f5537cddded9c8b1c3daa0a8d7c68d88f292d9804b270878e281fb654ab77b40248725b91046cba9885af649384766684316b6be6813f351f7fb214391ba6c8d0a6fb1a44089cdaf29501123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b65791001189cdaf295012001</string>
    <string name="ARK8rRzpTKbecLHO3yCiMMBaFHa2xCLZAJhtLVE=">ASyO+WIJmBqi+YFJPHKfX8DDLv8bZfqjVrL59TgD3CVmAfLH9elUqE+8hmVA</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801c62e98509808338085528011581576775e3701e7483e4a9d27b89ba63e3406db1b8a6a71d9aee723173fe3feb24455eb6ddbc5ffcab51dc7b4e6b4dc831a4e3daf4f2e3612337734667cfd134949655e2d05623badf75f5cde42eb4148d6aed4edd5914779e0ee360c905c0ffa3192c11d8560bc78c5bc4d84ff651bfca54ba67c5786059ae4f1701a4408e2f2bbe402123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118e2f2bbe4022001</string>
    <string name="ARK8rRxdjEShL1rCHntSr1uL6GEkhB4=">ASyO+WKJ42K6D7TXNXO5Os3dxLGmJeGdlYVsIRYUVif85iONYyTlSkwWhzQl</string>
</map>

위와같이 암호화되어 저장되어 있는것을 확인하였습니다.

오토레이아웃을 코딩으로 구현시 코드를 간결하게 작성하기 위해 SnapKit을 사용해 보겠습니다.

스냅킷은 DSL로 오토레이아웃을 쉽게 사용할 수 있는 기능을 제공합니다.

관련 링크는 하기와 같습니다.

https://github.com/SnapKit/SnapKit

 

SnapKit/SnapKit

A Swift Autolayout DSL for iOS & OS X. Contribute to SnapKit/SnapKit development by creating an account on GitHub.

github.com

 

변경 작업은 이전에 포스팅했던

https://xmobile.tistory.com/entry/IOS-오토레이아웃-제약조건-코딩으로-사용하기

를 활용 하도록 하겠습니다.

 

변경 전

private func addconstraints() {
        var constraints = [NSLayoutConstraint]()
        
        constraints.append(box1.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box2.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box3.heightAnchor.constraint(equalToConstant: 100))
        //box2와 box3 너비 같도록 설정 
        constraints.append(box2.widthAnchor.constraint(equalTo: box3.widthAnchor))
        constraints.append(box3.widthAnchor.constraint(equalTo: box2.widthAnchor))
        
        //box1 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box1 left가 위치함
        constraints.append(box1.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box1 right : 뷰콘트롤러뷰의 오른쪽에서 20만큼 떨어진 곳에 box1 right가 위치함
        constraints.append(box1.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box1 top :  뷰콘트롤러뷰의 위쪽에서 40만큼 떨어진 곳에 box1 top 위치함
        constraints.append(box1.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40))


        //box2 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box2 left가 위치함
        constraints.append(box2.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box2 top : box1의 아래쪽에서 40만큼 떨어진 곳에 box2 top 위치함
        constraints.append(box2.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))


        //box3 left : box2 오른쪽에서 20만큼 떨어진 곳에 box3 left가 위치함
        constraints.append(box3.leadingAnchor.constraint(equalTo: box2.trailingAnchor, constant: 20))
        //box3 right : 뷰콘트롤러뷰의 오른쪽에서 -20만큼 떨어진 곳에 box3 right가 위치함
        constraints.append(box3.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box3 top :  box1의 아래쪽에서 40만큼 떨어진 곳에 box3 top 위치함
        constraints.append(box3.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))


        //Activate(Applying)
        NSLayoutConstraint.activate(constraints)
    }

 

변경 후

private func addconstraints() {
        box1.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.leading.equalToSuperview().offset(20)
            make.trailing.equalToSuperview().offset(-20)
            make.top.equalToSuperview().offset(40)
        }
        
        box2.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.width.equalTo(box3.snp.width)
            make.leading.equalToSuperview().offset(20)
            make.top.equalTo(box1.snp.bottom).offset(40)
        }
        
        box3.snp.makeConstraints { (make) -> Void in
            make.height.equalTo(100)
            make.width.equalTo(box2.snp.width)
            make.leading.equalTo(box2.snp.trailing).offset(20)
            make.trailing.equalToSuperview().offset(-20)
            make.top.equalTo(box1.snp.bottom).offset(40)
        }
    }

위와 같이 소스코드가 간결해 지는것을 확인하였습니다. 

이상입니다. 

 

Realm Swift를 이용하면 효율적으로 안전하고 빠르고 지속적인 방법으로 앱의 모델 레이어를 작성할 수 있습니다.

https://realm.io/kr/docs/swift/latest/

 

Realm: 리액티브 모바일 애플리케이션을 손쉽고 빠르게 만드세요

Realm Swift is the first database built for mobile. An alternative to SQLite and Core Data that's fast, easy to use, and open source.

realm.io

의존성 설정 

pod 'RealmSwift', '~> 3.20.0'

 

사용해 보기 

1. entity 클래스 

import Foundation
import RealmSwift

class LottoEntity: Object {
    @objc dynamic var lottoid: Int = 0
    @objc dynamic var desc = ""
    @objc dynamic var url = ""
    @objc dynamic var regDate: Double = 0.0
    @objc dynamic var modDate: Double = 0.0
    
    override static func primaryKey() -> String? {
        return "lottoid"
    }
}

2. crud 만들기

import Foundation
import RealmSwift

public class DatabaseManager {
    static let shared = DatabaseManager()
    private var realm: Realm
    
    private init() {
        realm = try! Realm()
    }
    
    //id자동증가를 위한 함수 
    private func newID() -> Int {
        return realm.objects(LottoEntity.self).count+1
    }
    
    //삽입
    func insert(lottoEntity: LottoEntity) {
        lottoEntity.lottoid = newID()
        lottoEntity.regDate = Date().currentTimeMillis()
        
        try! realm.write {
            realm.add(lottoEntity)
        }
    }
    
    //업데이트 
    func update(lottoId: Int, desc: String?, url: String?) {
        let lottoEntity = selectById(lottoId: lottoId)
        if let workout = lottoEntity {
            try! realm.write {
                if let desc = desc{
                    workout.desc = desc
                }
                if let url = url{
                    workout.url = url
                }
                workout.modDate = Date().currentTimeMillis()
            }
        }
    }
    
    //삭제 object로 
    func delete(lottoEntity: LottoEntity) {
        try! realm.write {
            realm.delete(lottoEntity)
        }
    }
    
    //삭제 id로 
    func deleteById(lottiId: Int) {
        try! realm.write {
            if let entity = selectById(lottoId: lottiId) {
                realm.delete(entity)
            }
        }
    }
    
    //삭제 all
    func deleteAll() {
        try! realm.write {
            realm.deleteAll()
        }
    }
    
    //가져오기 id로 
    func selectById(lottoId: Int) -> LottoEntity? {
        let predicate = NSPredicate(format: "lottoid == %i", lottoId)
        return realm.objects(LottoEntity.self).filter(predicate).first
    }
    
    //가져오기 all
    func selectAll() -> Results<LottoEntity> {
        return realm.objects(LottoEntity.self)
    }
}

extension Date {
    func currentTimeMillis() -> Double {
        return Double(self.timeIntervalSince1970 * 1000)
    }
}

3. UNIT 테스트로 검증해보기  

func testExample() throws {
        //all 삭제
        DatabaseManager.shared.deleteAll()
        
        //insert
        var entity = LottoEntity()
        entity.url = "url"
        entity.desc = "desc"
        DatabaseManager.shared.insert(lottoEntity: entity)
        XCTAssert(DatabaseManager.shared.selectAll().count == 1)
        
        //update
        //desc 값 변경
        DatabaseManager.shared.update(lottoId: 1, desc: "desc_change", url: nil)
        print(DatabaseManager.shared.selectAll())
        if let selEntity = DatabaseManager.shared.selectById(lottoId: 1) {
            XCTAssert(selEntity.desc == "desc_change")
        }
        
        //insert
        entity = LottoEntity()
        entity.url = "url1"
        entity.desc = "desc2"
        DatabaseManager.shared.insert(lottoEntity: entity)
        XCTAssert(DatabaseManager.shared.selectAll().count == 2)
        
        //delete
        if let entity = DatabaseManager.shared.selectAll().first {
            DatabaseManager.shared.delete(lottoEntity: entity)
        }
        print(DatabaseManager.shared.selectAll())
        XCTAssert(DatabaseManager.shared.selectAll().count == 1)
        
        //delete all
        DatabaseManager.shared.deleteAll()
        XCTAssert(DatabaseManager.shared.selectAll().count == 0)
        print(DatabaseManager.shared.selectAll())
    }

하기와 같이 정상적으로 동작하는것을 확인해 보았습니다. 

NSLayoutConstraint를 이용하여 코딩을 통해서  간단한 뷰 배치를 하도록 하겠습니다.

뷰 3개를 이용하여 2가지 배치를 설정해 봅니다. 

private let box1: UIView = {
	let view = UIButton()
	view.backgroundColor = .blue
	view.translatesAutoresizingMaskIntoConstraints = false
	return view
}()
    
private let box2: UIView = {
    let view = UIButton()
    view.backgroundColor = .red
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()
    
private let box3: UIView = {
	let view = UIButton()
    view.backgroundColor = .yellow
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

...
override func viewDidLoad() {
	super.viewDidLoad()
	view.backgroundColor = .white
        
	view.addSubview(box1)
	view.addSubview(box2)
	view.addSubview(box3)
        
	addconstraints()
}

 

배치1

private func addconstraints() {
        var constraints = [NSLayoutConstraint]()
        
        constraints.append(box1.widthAnchor.constraint(equalToConstant: 100))
        constraints.append(box1.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box2.widthAnchor.constraint(equalToConstant: 100))
        constraints.append(box2.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box3.widthAnchor.constraint(equalToConstant: 100))
        constraints.append(box3.heightAnchor.constraint(equalToConstant: 100))
        
        //box1 left: 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box1 left가 위치함
        constraints.append(box1.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box1 top : 뷰콘트롤러뷰의 위쪽에서 40만큼 떨어진 곳에 box1의 top이 위치함  
        constraints.append(box1.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40))

        //box2 left: 뷰콘트롤러뷰의 왼쪽으로부터 20만큼 떨어진 곳에 box2 left 위치함 
        constraints.append(box2.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box2 top: box1 아래쪽이 40만큼 떨어진 곳에 box2 top이 위치 
        constraints.append(box2.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))

        //box3 right: 뷰콘트롤러뷰의 우측에서 -20만큼 떨어진 곳에 box3의 right가 위치함  
        constraints.append(box3.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box3 top: box2 아래쪽이 40만큼 떨어진 곳에 box3 top이 위치 
        constraints.append(box3.topAnchor.constraint(equalTo: box2.bottomAnchor, constant: 40))

        //Activate(Applying)
        NSLayoutConstraint.activate(constraints)
}

 

배치2

private func addconstraints() {
        var constraints = [NSLayoutConstraint]()
        
        constraints.append(box1.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box2.heightAnchor.constraint(equalToConstant: 100))
        constraints.append(box3.heightAnchor.constraint(equalToConstant: 100))
        //box2와 box3 너비 같도록 설정 
        constraints.append(box2.widthAnchor.constraint(equalTo: box3.widthAnchor))
        constraints.append(box3.widthAnchor.constraint(equalTo: box2.widthAnchor))
        
        //box1 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box1 left가 위치함
        constraints.append(box1.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box1 right : 뷰콘트롤러뷰의 오른쪽에서 20만큼 떨어진 곳에 box1 right가 위치함
        constraints.append(box1.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box1 top :  뷰콘트롤러뷰의 위쪽에서 40만큼 떨어진 곳에 box1 top 위치함
        constraints.append(box1.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40))

        //box2 left : 뷰콘트롤러뷰의 왼쪽에서 20만큼 떨어진 곳에 box2 left가 위치함
        constraints.append(box2.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20))
        //box2 top : box1의 아래쪽에서 40만큼 떨어진 곳에 box2 top 위치함
        constraints.append(box2.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))

        //box3 left : box2 오른쪽에서 20만큼 떨어진 곳에 box3 left가 위치함
        constraints.append(box3.leadingAnchor.constraint(equalTo: box2.trailingAnchor, constant: 20))
        //box3 right : 뷰콘트롤러뷰의 오른쪽에서 -20만큼 떨어진 곳에 box3 right가 위치함
        constraints.append(box3.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20))
        //box3 top :  box1의 아래쪽에서 40만큼 떨어진 곳에 box3 top 위치함
        constraints.append(box3.topAnchor.constraint(equalTo: box1.bottomAnchor, constant: 40))

        //Activate(Applying)
        NSLayoutConstraint.activate(constraints)
    }

 

이상으로 제약조건을 코딩으로 관계에 대한 설정을 통해서 사용법을 간단하게 사용해 보았습니다.

'IOS' 카테고리의 다른 글

[IOS] RxSwfit 사용하기  (0) 2021.01.04
[IOS] TrustKit 사용해보기 (SSL적용)  (0) 2020.12.22
[IOS] SnapKit 사용해보기  (0) 2020.10.19
[IOS] Realm Database CRUD 사용해보기  (0) 2020.10.17
[IOS] CocoaPods 사용하기  (0) 2020.10.12

JPA는 기본적으로 테이블의 CRUD 메소드를 제공해주기 때문에 SQL문을 직접 사용할 필요가 없습니다. 

그러나 여러 테이블의 데이터를 조합하기 위해서 불필요한 데이터를 가져올 수 밖에 없는데 이러한 점을 해결하기 위해 JPQL (Java Persistence Query Language라는 쿼리 언어를 사용하고

QueryDsl은 JPQL의 빌더 역할을 하는 오픈소스입니다. 

 

gradle 설정

plugins {
     id 'org.springframework.boot' version '2.3.2.RELEASE'
     id 'io.spring.dependency-management' version '1.0.9.RELEASE'
     id 'java'
     id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
…

dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-web'
     
     //log
     annotationProcessor 'org.projectlombok:lombok'
     compileOnly 'org.projectlombok:lombok'
     
     //querydsl
     compile("com.querydsl:querydsl-jpa") // querydsl
     compile("com.querydsl:querydsl-apt") // querydsl

     testImplementation('org.springframework.boot:spring-boot-starter-test') {
         exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
     }
     
     //db
     compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.4.1'  
     compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
}

//querydsl 
def querydslSrcDir = 'src/main/generated'
querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}
compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}
configurations {
    querydsl.extendsFrom compileClasspath
}
sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}
//querydsl

 

사용하기 

1. Entity클래스 만들기 

@Entity
@Table(name="lo_mst")  
@Getter
@Setter
public class EntityMst{
     @Id
     @Column(name = "id")
     @GeneratedValue(strategy=GenerationType.IDENTITY)
     private int id;
     @Column(name = "_order")
     private int _order;
     @Column(name = "num1")
     private int num1;
     @Column(name = "num2")
     private int num2;
     @Column(name = "num3")
     private int num3;
     @Column(name = "num4")
     private int num4;
     @Column(name = "num5")
     private int num5;
     @Column(name = "num6")
     private int num6;
     @Column(name = "num7")
     private int num7;
     @Column(name = "win_date")
     private String winDate;
     @Column(name = "reg_dt")
     private LocalDateTime regDt;
}

Entity클래스를 생성 후 빌드를 하게되면 gradle에서 설정한 generated폴더에 클래스가 생성하게 됩니다. 

 

2. QueryDsl 사용 

아래와 같이 생성된 클래스, 변수에 접근하여 쿼리를 작성할 수 있습니다. 

MainDto.LottoInfo result = queryFactory.select(Projections.bean(MainDto.LottoInfo.class 
                 , ExpressionUtils.as(
                        JPAExpressions.select(entityMst._order.max())
                        .from(entityMst),  "lastOrder")
                 ,entityMst._order
                 ,entityMst.num1
                 ,entityMst.num2
                 ,entityMst.num3
                 ,entityMst.num4
                 ,entityMst.num5
                 ,entityMst.num6
                 ,entityMst.num7
                 ,entityMst.winDate
                 ,entityMstEtc.etc
                 ,entityMstEtc.totalSalesPrc
                 ,entityMstEtc.payLimit
                 ))
               .from(entityMst)
               .innerJoin(entityMstEtc)
               .on(entityMst._order.eq(entityMstEtc._order))
               .where(builder)
               .orderBy(entityMst._order.desc())
               .fetchFirst();

 

CocoaPods는 Swift 프로젝트에서 타사 라이브러리 의존성을 설치하고 관리하는 방법을 제공합니다.

 

준비

1. CocoaPods 설치

sudo gem install cocoapods

 

2. CocoaPods원격 저장소 파일들을 로컬로 가져옵니다. 

pod setup --verbose

 

사용

1. Xcode Project 생성 후 terminal로 해당 폴더로 이동 후 pod init 커맨드로 Podfile을 생성합니다. 

 

2 Podfile에 의존성 추가 

3. pod명령으로 의존성 설치 

pod install

 

4. .xcworkspace파일로 프로젝트 열기 

.xcworkspace 파일로 프로젝트를 열어야합니다. 그렇지 않으면 빌드 오류가 발생합니다.

이상으로 CocoaPods를 사용하여 

의존성을 추가했습니다

 

ViewModel 에서 api통신후 LiveData를 참조하여 정상적으로 갱신되었는지 확인하여 

정상 동작하는지 테스트를 진행해 보도록 하겠습니다. 

 

LiveData확인을 위한 확장함수를 활용하여 진행 하도록 합니다. 

https://github.com/android/architecture-components-samples/blob/master/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/LiveDataTestUtil.kt

 

android/architecture-components-samples

Samples for Android Architecture Components. . Contribute to android/architecture-components-samples development by creating an account on GitHub.

github.com

 

build.gradle

// Testing dependencies
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.5"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.28.3-alpha"
androidTestImplementation "androidx.arch.core:core-testing:2.0.0"
// Espresso dependencies
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.1.1"
androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
androidTestImplementation "androidx.test.espresso:espresso-intents:3.1.1"
// Assertions
androidTestImplementation "androidx.test.ext:junit:1.1.0"
androidTestImplementation "com.google.truth:truth:0.42"
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
androidTestImplementation "androidx.work:work-testing:2.1.0"
androidTestImplementation "com.google.dagger:hilt-android-testing:2.28.3-alpha"
testImplementation "junit:junit:4.12"

 

MainRepository

class MainRepository(private val apiService: ApiService) {
    suspend fun getLotto(order: Int) = apiService.getLotto(order)
}

 

ApiService

interface ApiService {
    @GET("api/lotto")
    suspend fun getLotto(@Query("order") order: Int ): HomeDto

    companion object {
        private const val BASE_URL = "http://192.168.0.103:8181/"

        fun create(): ApiService {
            val logger = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }

            val client = OkHttpClient.Builder()
                .addInterceptor(logger)
                .build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService::class.java)
        }
    }
}

 

LiveDataTestUtil

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()
        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }
    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

 

HomeViewModel

getLotto메소드는 retrofit을 사용하여 서버 api를 호출 후 homeDto 라이브 데이터를 갱신합니다.

class HomeViewModel @ViewModelInject constructor(val mainRepository: MainRepository
) : ViewModel() {
    var isLoading = MutableLiveData<Boolean>()
    var homeDto = MutableLiveData<HomeDto>()

    fun getLotto(order: Int) {
        try {
            isLoading.postValue(true)
            viewModelScope.launch {
                homeDto.postValue(mainRepository.getLotto(order))
            }
        } catch (e: Exception) {
            homeDto.postValue(null)
        } finally {
            isLoading.postValue(false)
        }
    }
}

 

HomeViewModelTest

Api 호출후 livedata를 확인하도록 합니다. 

@HiltAndroidTest
@ExperimentalCoroutinesApi
class HomeViewModelTest {
    private lateinit var viewModel: HomeViewModel
    private val hiltRule = HiltAndroidRule(this)
    private val instantTaskExecutorRule = InstantTaskExecutorRule()

    @get:Rule
    val rule = RuleChain
        .outerRule(hiltRule)
        .around(instantTaskExecutorRule)

    @Inject
    lateinit var mainRepository: MainRepository

    @Before
    fun setUp() {
        hiltRule.inject()
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        viewModel = HomeViewModel(mainRepository)
    }

    @Test
    fun callRequest()  {
        //호출 후 livedata를 확인 
        viewModel.getLotto(0)
        assertTrue(viewModel.homeDto.getOrAwaitValue()._order > 0 )
    }
}

정상적으로 테스트 확인. 

이를 바탕으로 viewmodel livedata을 검증하는 방법에 대해서 확인해 보았습니다.  

'Android' 카테고리의 다른 글

[Android] Activity Result API 사용해보기  (0) 2020.10.22
[Android] EncryptedSharedPreferences 사용해보기  (0) 2020.10.20
[Android] Hilt 사용해보기  (0) 2020.10.09
[Android] 권한 요청  (0) 2020.10.05
[Android] 블루투스 연결  (0) 2020.10.04

Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다.

수동 종속 항목 삽입예는 

class LoginActivity: Activity() {
    private lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val apiService = ApiService(retrofit)
        val mainRepository = MainRepository(apiService)
        // Lastly, create an instance of MainViewModel with mainRepository
        mainViewModel = MainViewModel(mainRepository)
    }
}

이 접근 방식은 다음과 같은 문제가 있습니다.

1. 상용구 코드가 많습니다. 코드의 다른 부분에서 MainViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.

2. 종속성은 순서대로 선언해야 합니다. RemoteRepository을 만들려면 MainViewModel 전에 인스턴스화해야 합니다.

3. 객체를 재사용하기가 어렵습니다. 여러 기능에 걸쳐 RemoteRepository를 재사용하려면 싱글톤 패턴을 따르게 해야 합니다. 모든 테스트가 동일한 싱글톤 인스턴스를 공유하므로 싱글톤 패턴을 사용하면 테스트가 더 어려워집니다.

 

 

Hilt를 사용하여 해결하기 

설정  

root build.gradle

buildscript {

    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

 

2.app/build.gradle

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.29.1-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.29.1-alpha"
    implementation "androidx.hilt:hilt-common:1.0.0-alpha02"
    implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02"
    kapt "androidx.hilt:hilt-compiler:1.0.0-alpha02"
}

 

3 .Hilt는 자바 8 기능을 사용합니다. 프로젝트에서 자바 8을 사용 설정하려면 app/build.gradle 파일에 다음을 추가합니다.

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

 

구현 .

1. 애플리케이션 클래스에 Hilt를 설정합니다. 

@HiltAndroidApp
class ExampleApplication : Application() { ... }

 

2.Hilt 모듈은 @Module 주석이 지정된 클래스입니다.

   인스턴스를 제공할 항목을 @Provides를 사용하여 Hilt에 알려줍니다. 

@Module
@InstallIn(ApplicationComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideApiService(): ApiService {
        return ApiService.create()
    }

    @Provides
    fun provideMainRepository(apiService: ApiService): MainRepository {
        return MainRepository(apiService)
    }
}

Component 수명주기 

생성된 구성요소  생성 위치 제거 위치
ApplicationComponent Application#onCreate()   Application#onDestroy()
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super()  제거된 뷰
ViewWithFragmentComponent View#super() 제거된 뷰
ServiceComponent Service#onCreate()  Service#onDestroy()

 

3. Jetpack라이브러리 ViewModel의 경우 ViewModel 객체의 생성자에서 @ViewModelInject 을 사용하여 ViewModel을 제공할 수 있습니다

class MainViewModel @ViewModelInject constructor(val mainRepository: MainRepository) : ViewModel() {
    fun getLotto(order: Int) = liveData(Dispatchers.IO) {
        emit(Resource.loading(data = null))
        try {
            emit(Resource.success(data = mainRepository.getLotto(order)))
        } catch (exception: Exception) {
            emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
        }
    }
}

4. 종목 항목 삽입  

    @AndroidEntryPoint를 사용하여 Android 클래스에 종속 항목을 제공합니다.

@AndroidEntryPoint
class HomeFragment : Fragment() {
    //@Inject lateinit var mainRepository: MainRepository
    private val viewModel: MainViewModel by viewModels()
    ...
}

Hilt는 현재 다음 Android 클래스를 지원합니다.

  • Application(@HiltAndroidApp을 사용하여)

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

 

마무리

Hilt와 같이 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다.

종속 항목 삽입을 구현하면 다음과 같은 이점을 누릴 수 있습니다.

  • 코드 재사용 가능

  • 리팩터링 편의성

  • 테스트 편의성

developer.android.com/training/dependency-injection/hilt-android?hl=ko

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스와 종속 항목을 수동으로 구성�

developer.android.com

 

모든 Android 앱은 액세스가 제한된 샌드박스에서 실행됩니다. 자체 샌드박스 밖에 있는 리소스나 정보를 앱이 사용해야 하는 경우에는 앱이 적절한 권한을 요청해야 합니다. 앱에 권한이 필요하다고 선언하려면 권한을 앱 매니페스트에 표시한 후 사용자가 런타임에 각 권한을 승인하도록 요청합니다(Android 6.0 이상). 

 

1.매니페스트에 권한 추가

<uses-permission android:name="android.permission.CAMERA" />

 

2 6.0이상 버전에 대해서 요청을 합니다. 

private fun setupUI() {
    binding.request.setOnClickListener {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            showCamera()
        } else {
            showCameraWithPermission(Manifest.permission.CAMERA
                , "게임에 필요한 권한 필요” //거절한 경우 필요 설명 
                , "카메라 권한 요청”) //거절한 경우 버튼 설명 
        }
    }
}

 

3. 권한 요청 

@RequiresApi(Build.VERSION_CODES.M)
private fun showCameraWithPermission(permissionName: String, reasonDesc: String, reasonButtonText: String) {
    if (checkSelfPermission(permissionName) == PackageManager.PERMISSION_GRANTED) {
        //허용
        showCamera()
    } else {
        if (shouldShowRequestPermissionRationale(permissionName)) {
            //거절한 경우 설명 후 재 요청.
            Snackbar.make(binding.mainLayout, reasonDesc, Snackbar.LENGTH_INDEFINITE)
                .setAction(reasonButtonText
                ) {
                    requestPermissions(
                        arrayOf(permissionName),
                        PERMISSION_REQUEST_CAMERA
                    )
                }.show()
        } else {
            //거절기록 없을때 최초 false 권한 요청
            requestPermissions(
                arrayOf(permissionName),
                PERMISSION_REQUEST_CAMERA
            )
        }
    }
}

 

4. 콜백 응답 

@RequiresApi(Build.VERSION_CODES.M)
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == PERMISSION_REQUEST_CAMERA) {
        if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "권한 성공", Toast.LENGTH_SHORT).show()
            showCamera()
        } else {
            if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
                Toast.makeText(this, "권한 거절", Toast.LENGTH_SHORT).show()
            } else {
                //거부 및 다시 묻지 않기
                Toast.makeText(this,  "앱 기능 실행을 위해서는 설정화면에서 권한을 허용해주세요 ", Toast.LENGTH_LONG).show();
            }
        }
    }
}

+ Recent posts