How to share HttpClient between Multiplatform Ktor and Coil? - kotlin-multiplatform

I want to use Coil image library to load images from the api with the same cookie that was set before. Therefore I want to use the same HttpClient both for my Ktor networking calls and for Image Loading with Coil.
How can I share the same HttpClient between Ktor and Coil? I assume, I need to adjust dependencies somehow, but I can't wrap my head around it.
My KtorApiImpl in shared module
class KtorApiImpl(log: Kermit) : KtorApi {
val baseUrl = BuildKonfig.baseUrl
// If this is a constructor property, then it gets captured
// inside HttpClient config and freezes this whole class.
#Suppress("CanBePrimaryConstructorProperty")
private val log = log
override val client = HttpClientProvider().getHttpClient().config {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
log.v("Network") { message }
}
}
level = LogLevel.INFO
}
}
init {
ensureNeverFrozen()
}
override fun HttpRequestBuilder.apiUrl(path: String) {
url {
takeFrom(baseUrl)
encodedPath = path
}
}
override fun HttpRequestBuilder.json() {
contentType(ContentType.Application.Json)
}
}
actual HttpClientProvider in androidMain
var cookieJar: CookieJar = object : CookieJar {
private val cookieStore: HashMap<String, List<Cookie>> = HashMap()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
cookieStore[url.host] = cookies
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = cookieStore[url.host]
return cookies ?: ArrayList()
}
}
actual class HttpClientProvider actual constructor() {
actual fun getHttpClient(): HttpClient {
return HttpClient(OkHttp) {
engine {
preconfigured = getOkHttpClient()
}
}
}
}
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.cookieJar(cookieJar)
.build()
}
ImageLoaderFactory in androidApp - how to use an HttpClient instead of creating new?
class CoilImageLoaderFactory(private val context: Context) : ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(context)
.availableMemoryPercentage(0.25) // Use 25% of the application's available memory.
.crossfade(true) // Show a short crossfade when loading images from network or disk.
.componentRegistry {
add(ByteArrayFetcher())
}
.okHttpClient {
// Create a disk cache with "unlimited" size. Don't do this in production.
// To create the an optimized Coil disk cache, use CoilUtils.createDefaultCache(context).
val cacheDirectory = File(context.filesDir, "image_cache").apply { mkdirs() }
val cache = Cache(cacheDirectory, Long.MAX_VALUE)
// Lazily create the OkHttpClient that is used for network operations.
OkHttpClient.Builder()
.cache(cache)
.build()
}
.build()
}
}
Koin dependencies in androidApp
#Suppress("unused")
class MainApp : Application() {
override fun onCreate() {
super.onCreate()
initKoin(
module {
single<Context> { this#MainApp }
single<AppInfo> { AndroidAppInfo }
single { CoilImageLoaderFactory(get<Context>())}
single<SharedPreferences> {
get<Context>().getSharedPreferences("MAIN_SETTINGS", Context.MODE_PRIVATE)
}
single {
{ Log.i("Startup", "Hello from Android/Kotlin!") }
}
}
)
}
}
And then Main Activity
class MainActivity : AppCompatActivity() {
val loaderFactory: CoilImageLoaderFactory by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalImageLoader provides loaderFactory.newImageLoader()) {
MainTheme {
ProvideWindowInsets {
Surface {
MainScreen()
}
}
}
}
}
}
}

I accessed the OkHttpClient from ImageLoader with
class CoilImageLoaderFactory(private val context: Context) : ImageLoaderFactory, KoinComponent {
val ktorApiImpl: KtorApi by inject()
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(context)
.componentRegistry {
add(ByteArrayFetcher())
}
.okHttpClient {
val config = ktorApiImpl.client.engine.config as OkHttpConfig
config.preconfigured as OkHttpClient
}
.build()
}

You could use ImageLoader.Builder.callFactory{} to provide your own Call.Factory used for network requests. The downside is you would have to map whatever type your KtorApiImpl returns to okttp3.Response which Coil understands.
Here’s a sample that describes how to implement the Call.Factory interface and provide it to Coil’s ImageLoader
ImageLoader.Builder(context)
.callFactory {
Call.Factory {
object: Call {
private var job: Job? = null
override fun clone(): Call {
TODO(“Not yet implemented”)
}
override fun request(): Request {
return it
}
override fun execute(): Response {
return runBlocking {
// Call KTOR client here
}
}
override fun enqueue(responseCallback: Callback) {
// Use a proper coroutines scope
job = GlobalScope.launch {
// Call KTOR client here
}
}
override fun cancel() {
job?.cancel()
}
override fun isExecuted(): Boolean {
return job?.isCompleted ?: false
}
override fun isCanceled(): Boolean {
return job?.isCancelled ?: false
}
override fun timeout(): Timeout {
// Your Timeout here
}
}
}
}

Related

How to mock final class and static func for unit test with Swift?

I have a class with static function like this.
UserAuthentication.swift
final class UserAuthentication {
/// Check is logged in
/// - Returns: boolean true is login, false not login
#objc public static func isLogin() -> Bool {
return true
}
}
I want to write a unit test for checkTermAndCondition function which call it in my HomeInteractor class
HomeInteractor.swift
class HomeInteractor: HomeInteractorBusinessLogic {
var presenter: HomePresenterInterface!
var worker: HomeWorker = HomeWorker(with: HomeService())
func checkTermAndCondition() {
if UserAuthentication.isLogin() {
///do true case
} else {
///do fasle case
}
}
}
Have anyone ever done it ?
Appreciate any help from you.
You cannot mock a static method on a final class.
You should either change the method to class and make the class non-final or even better, inject UserAuthentication as a protocol to HomeInteractor and in your unit tests, inject a mock type rather than your real production type.
protocol UserAuthenticator {
/// Check is logged in
/// - Returns: boolean true is login, false not login
static func isLogin() -> Bool
}
final class UserAuthentication: UserAuthenticator {
#objc public static func isLogin() -> Bool {
return true
}
}
final class UserAuthenticationMock: UserAuthenticator {
static var shouldLogin: Bool = false
static func isLogin() -> Bool {
shouldLogin
}
}
class HomeInteractor: HomeInteractorBusinessLogic {
var presenter: HomePresenterInterface!
var worker: HomeWorker = HomeWorker(with: HomeService())
let userAuthenticator: UserAuthenticator.Type
init(userAuthenticator: UserAuthenticator.Type) {
self.userAuthenticator = userAuthenticator
}
func checkTermAndCondition() {
if userAuthenticator.isLogin() {
} else {
}
}
}
Inject UserAuthentication.self for your prod code, while UserAuthenticationMock.self for the tests.
let prodHomeInteractor = HomeInteractor(userAuthenticator: UserAuthentication.self)
let testHomeInteractor = HomeInteractor(userAuthenticator: UserAuthenticationMock.self)

How to provide dependency of List Adapter class using dagger hilt? - when adapter class takes interface as a parameters

suppose this is a interface
interface Listener {
fun deleteOnClick(position:Int,userId:Int)
fun updateOnClick(position: Int,userId: Int,name:String,phoneNo:Long)
}
And this is my Adapter class, where i am passing listener interface as a parameter 👇🏻
class PhoneAdapter
#Inject
constructor(private val listener: Listener) : ListAdapter<Phone,PhoneAdapter.PhoneViewHolder>(Diff) {
inner class PhoneViewHolder(private val binding: EachRowBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(phone: Phone){
binding.apply {
name.text = phone.name
phoneNo.text = phone.phoneNo.toString()
delete.setOnClickListener {
listener.deleteOnClick(adapterPosition,phone.userId)
}
root.setOnClickListener {
listener.updateOnClick(adapterPosition,phone.userId,phone.name,phone.phoneNo)
}
}
}
}
object Diff : DiffUtil.ItemCallback<Phone>(){
override fun areItemsTheSame(oldItem: Phone, newItem: Phone): Boolean {
return oldItem.userId == newItem.userId
}
override fun areContentsTheSame(oldItem: Phone, newItem: Phone): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhoneViewHolder {
return PhoneViewHolder(EachRowBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: PhoneViewHolder, position: Int) {
val phone = getItem(position)
if(phone != null){
holder.bind(phone)
}
}
}
And MainActivity implementing this interface👇🏻
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), Listener {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
....
....
}
So my question is how to provide dependency of Listener interface..

Android LiveData: Unable to receive updates on Observable?

I am using android Architecture Component LiveData to notify UI using Observable LiveData but it is not getting triggered. below is the code snippet.
AuthRepository
class AuthRepository(private val repository:Avails) {
fun auth(mobile: String): LiveData<Boolean>{
var data: MutableLiveData<Boolean> = MutableLiveData()
repository.auth(prepareHeaders(), AuthRequest(mobile))
.enqueue(object : Callback<AuthResponse>{
override fun onResponse(call: Call<AuthResponse>, response: Response<AuthResponse>) {
if(response.isSuccessful){
data.value = true
}
}
override fun onFailure(call: Call<AuthResponse>, t: Throwable) {
data.value = false
}
})
return data
}
}
LoginViewModel
class LoginViewModel(private val repository: AuthRepository) : ViewModel() {
var _showOtpScreen: MutableLiveData<Boolean> = MutableLiveData()
fun auth(mobile: String){
_showOtpScreen.value = repository.auth(mobile).value
}
}
LoginFragment
class LoginFragment : Fragment() {
private lateinit var loginViewModel: LoginViewModel
companion object {
private const val sTag: String = "LoginFragment"
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val authRepository = AuthRepository(AvailsClient.retrofit.create(Avails::class.java))
loginViewModel = ViewModelProviders.of(this,LoginViewModelFactory(authRepository)).get(LoginViewModel::class.java)
loginViewModel._showOtpScreen.observe(this, Observer {
if(it != null){
if(it){
Log.e(sTag,"OTP RECEIVED")
findNavController().navigate(R.id.action_loginFragment_to_verifyOtpFragment)
}else{
Log.e(sTag,"Failed to get OTP")
}
}
})
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_login, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnContinue.setOnClickListener {
loginViewModel.auth(edtPhoneNumber.text.toString())
}
}
}
Above code is not able to Observe _showOtpScreen it is only called once with null value and never gets called again when service call completes.
Event wrapper is the solution for above problem.

Detect in runtime if is production code or test code in Swift

What is the best way to detect in runtime if the code is running a test or the production app.
Basically I want to allow a setter just for testing purposes. Something like:
class LoginService {
private static var LoginService instance = LoginService();
public var sharedInstance: LoginService {
get{
return instance;
}
set{
if(inRunningTests()){
instance = newValue;
} else {
fatalError("This setter is just for testing")
}
}
}
static func isRunningTests() -> Bool {
// ????
}
}
My working solution
static var isRunningTests : Bool {
get {
return NSClassFromString("XCTest") != nil;
}
}
For unit tests you can use;
func isTesting() -> Bool {
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
return false
}
return true
}

Swift protocol with associatedtype (PAT)

What I want to achieve is to wait for all service calls to complete. I know that it can be done with GCD, but I'm looking for more Object Oriented Approach. Here is what I've so far:
First services should notify delegate for their completion, so we will need a protocol for that:
protocol ParallelServiceDelegate: class {
func serviceDidCompleted()
}
Services are Alamofire requests and we are getting their response as:
enum ServiceResult {
case Success(NSDictionary)
case Failure(NSError)
}
My design is to add Facade (wrapper) over this methods. This is the abstract:
import ObjectMapper
protocol ParallelService: class {
associatedtype ItemType: Mappable
var item: ItemType? { get }
var error: NSError? { get }
var isCompleted: Bool { get }
weak var delegate: ParallelServiceDelegate? { get set }
// TODO: Pass params
func start()
func handleRequestCompletion(result: ServiceResult)
}
private var psiAssociationKey: UInt8 = 0
private var pseAssociationKey: UInt8 = 0
private var psdAssociationKey: UInt8 = 0
extension ParallelService {
var item: ItemType? {
return _item
}
var error: NSError? {
return _error
}
var isCompleted: Bool {
return item != nil || error != nil
}
weak var delegate: ParallelServiceDelegate? {
get {
let object = objc_getAssociatedObject(self, &psdAssociationKey)
let wrapper = object as? WeakWrapper<ItemType?>
return wrapper?.value as? ParallelServiceDelegate
}
set(newValue) {
objc_setAssociatedObject(self,
&psdAssociationKey,
WeakWrapper(value: newValue),
.OBJC_ASSOCIATION_RETAIN)
}
}
func handleRequestCompletion(result: ServiceResult) {
switch result {
case .Success(let json):
_item = map(json, object: ItemType.self)
case .Failure(let error):
_error = error
}
}
// Degfault is nothing
func start() {}
// MARK: - Private
private var _item: ItemType? {
get {
let object = objc_getAssociatedObject(self, &psiAssociationKey)
let wrapper = object as? WeakWrapper<ItemType?>
return wrapper?.value as? ItemType
}
set(newValue) {
objc_setAssociatedObject(self,
&psiAssociationKey,
WeakWrapper(value: newValue),
.OBJC_ASSOCIATION_RETAIN)
}
}
private var _error: NSError? {
get {
let object = objc_getAssociatedObject(self, &pseAssociationKey)
return object as? NSError
}
set(newValue) {
objc_setAssociatedObject(self,
&pseAssociationKey,
newValue,
.OBJC_ASSOCIATION_RETAIN)
}
}
}
And Specific Service Facade implementation:
class EmployeesParallelService: ParallelService {
typealias ItemType = Employee
func start() {
Service.emploeesList(callback: handleRequestCompletion)
}
}
class InformationParallelService: ParallelService {
typealias ItemType = Information
func start() {
Service.information(callback: handleRequestCompletion)
}
}
Service caller - Knows nothing about services it just starts all and waits for them to complete:
class ParallelServiceCaller {
private var services: [ParallelService] = []
// MARK: - Lifecycle
func addParallelService(service: ParallelService) {
service.delegate = self
self.services.append(service)
}
// MARK: - Public
func start() {
for service in services {
service.start()
}
}
}
extension ParallelServiceCaller: ParallelServiceDelegate {
func serviceDidCompleted() {
for service in services {
// !!! wait
if !service.isCompleted {
return
}
}
// TODO: Notify delegate
}
}
Latter I want to use it like this:
let caller = ParallelServiceCaller()
caller.addParallelService(EmployeesParallelService())
caller.addParallelService(InformationParallelService())
caller.start()
However I got problem in the implementation of the ParallelServiceCaller class. I'm getting the following error:
Protocol 'ParallelService' can only be used as a generic constraint
because it has Self or associated type requirements
Any idea how to avoid this error?
Update 07/07/16:
I'm still not able to understand how to use PATs. However I took slightly different approach and now I'm using visitor pattern. Here is my playground code, it may be helpful for someone:
//: Playground - noun: a place where people can play
import UIKit
// Mocks
enum ServiceResult {
case Success(NSDictionary)
case Failure(NSError)
}
protocol Mappable { }
typealias CompletionHandlerType = (result: ServiceResult) -> Void
class Service {
class func emploeesList(start: Int? = nil,
max: Int? = nil,
timestamp: Int? = nil,
callback: CompletionHandlerType) {
callback(result: .Success(NSDictionary()))
}
class func information(timestamp: Int? = nil,
callback: CompletionHandlerType) {
callback(result: .Failure(NSError(domain: "", code: 1, userInfo: nil)))
}
}
class EmployeesList: Mappable {}
class Information: Mappable {}
// Actual Implementation
// Visitor
protocol ParallelServiceCallerProtocol {
func call(service: EmployeesListParallelService)
func call(service: InformationParallelService)
}
// Element
protocol ParallelServiceProtocol {
func start(visitor: ParallelServiceCallerProtocol)
}
// Concrete Elements
class EmployeesListParallelService: ParallelServiceProtocol {
func start(visitor: ParallelServiceCallerProtocol) { visitor.call(self) }
}
class InformationParallelService: ParallelServiceProtocol {
func start(visitor: ParallelServiceCallerProtocol) { visitor.call(self) }
}
// Concrete Visitor Delegate - defines callback for async tasks
protocol ParallelServiceCallerDelegateProtocol: class {
func didCompleteParellelServiceWithResult(service: ParallelServiceProtocol, result: ServiceResult)
}
// Concrete Visitor - make API calls
class ParallelServiceCaller <T: ParallelServiceCallerDelegateProtocol>: ParallelServiceCallerProtocol {
private unowned let delegate: T
init(delegate: T) {
self.delegate = delegate
}
func call(service: EmployeesListParallelService) {
Service.emploeesList { [unowned self] (result) in
self.delegate.didCompleteParellelServiceWithResult(service, result: result)
}
}
func call(service: InformationParallelService) {
Service.information { (result) in
self.delegate.didCompleteParellelServiceWithResult(service, result: result)
}
}
}
// Service Result In Context
enum SynchronizationServiceResult {
case Employees(ServiceResult)
case Information(ServiceResult)
}
// Concrete Visitor - Wraps API Result And Gives Context
class ParallelServiceParser: ParallelServiceCallerProtocol {
var result: SynchronizationServiceResult?
private let serviceResult: ServiceResult
init(serviceResult: ServiceResult) {
self.serviceResult = serviceResult
}
func call(service: EmployeesListParallelService) {
result = .Employees(serviceResult)
}
func call(service: InformationParallelService) {
result = .Information(serviceResult)
}
}
// Delegate that notifies for completion of all calls
protocol ParallelServiceManagerDelegateProtocol: class {
func didCompleteAllServicesWithResults(results: [SynchronizationServiceResult])
}
// Manager - starts all calls and adds context to returned results - knows nothing about calls
class ParallelServiceManager<T where T: ParallelServiceManagerDelegateProtocol> {
private let services: [ParallelServiceProtocol]
private unowned let delegate: T
// Keep Caller Visitors in Memory or they will be dealocated
private var callers: [ParallelServiceCaller<ParallelServiceManager>] = []
private var completed: [SynchronizationServiceResult] = [] {
didSet {
if completed.count == services.count {
self.delegate.didCompleteAllServicesWithResults(completed)
self.callers.removeAll()
}
}
}
init(services: [ParallelServiceProtocol], delegate: T) {
self.services = services
self.delegate = delegate
}
func start() {
visitAllServices { (service) in
let caller =
ParallelServiceCaller<ParallelServiceManager>(delegate: self)
service.start(caller)
self.callers.append(caller)
}
}
private func visitAllServices(perform: ParallelServiceProtocol -> () ) {
for service in self.services {
perform(service)
}
}
}
extension ParallelServiceManager: ParallelServiceCallerDelegateProtocol {
func didCompleteParellelServiceWithResult(service: ParallelServiceProtocol,
result: ServiceResult) {
// No need to persist parser visitor
let caller = ParallelServiceParser(serviceResult: result)
service.start(caller)
completed.append(caller.result!)
}
}
// Example Usage
class SynchronizationService {
private lazy var services: [ParallelServiceProtocol] = {
return [EmployeesListParallelService(), InformationParallelService()]
}()
func start() {
let manager = ParallelServiceManager<SynchronizationService>(services: services, delegate: self)
manager.start()
}
}
extension SynchronizationService: ParallelServiceManagerDelegateProtocol {
func didCompleteAllServicesWithResults(results: [SynchronizationServiceResult]) {
for result in results {
switch result {
case .Employees(let result):
// TODO:
print("\(result)") // Should Return Success
case .Information(let result):
// TODO:
print("\(result)") // Should Return Failure
}
}
}
}
let sync = SynchronizationService()
sync.start()

Resources