diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c8fbed8..b2d252b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,13 +3,32 @@ package="dev.carlos.acronyms" > + + + android:hardwareAccelerated="true" + > + + + + + + + + + + + diff --git a/app/src/main/java/dev/carlos/acronyms/MainApplication.kt b/app/src/main/java/dev/carlos/acronyms/MainApplication.kt new file mode 100644 index 0000000..ea7d95c --- /dev/null +++ b/app/src/main/java/dev/carlos/acronyms/MainApplication.kt @@ -0,0 +1,32 @@ +package dev.carlos.acronyms + +import android.app.Application +import dev.carlos.acronyms.di.appModules +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import timber.log.Timber + +@Suppress("unused") +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + setupLogger() + setupDi() + } + + private fun setupLogger() { + Timber.plant(Timber.DebugTree()) + } + + private fun setupDi() { + startKoin { + androidContext(this@MainApplication) + + modules( + listOf( + appModules + ) + ) + } + } +} diff --git a/app/src/main/java/dev/carlos/acronyms/di/AppModules.kt b/app/src/main/java/dev/carlos/acronyms/di/AppModules.kt new file mode 100644 index 0000000..30e1cff --- /dev/null +++ b/app/src/main/java/dev/carlos/acronyms/di/AppModules.kt @@ -0,0 +1,17 @@ +package dev.carlos.acronyms.di + +import dev.carlos.acronyms.BuildConfig +import dev.carlos.acronyms.viewmodel.NavigationViewmodel +import dev.carlos.core.domain.network.RemoteClient +import dev.carlos.core.navigation.NavigationRouter +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val appModules = module { + // Retrofit + single { RemoteClient(BuildConfig.BASE_ENDPOINT) } + + // Navigation + single { NavigationRouter() } + viewModel { NavigationViewmodel() } +} diff --git a/app/src/main/java/dev/carlos/acronyms/viewmodel/NavigationViewmodel.kt b/app/src/main/java/dev/carlos/acronyms/viewmodel/NavigationViewmodel.kt new file mode 100644 index 0000000..0329917 --- /dev/null +++ b/app/src/main/java/dev/carlos/acronyms/viewmodel/NavigationViewmodel.kt @@ -0,0 +1,18 @@ +package dev.carlos.acronyms.viewmodel + +import android.os.Bundle +import androidx.annotation.IdRes +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dev.carlos.core.navigation.NavigationEvent + +class NavigationViewmodel : ViewModel() { + + val navigate = MutableLiveData>() + + fun onNavigationEvent(event: NavigationEvent) { + when (event) { + else -> throw NoWhenBranchMatchedException("Undefined navigation event parent") + } + } +} diff --git a/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt b/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt new file mode 100644 index 0000000..8d5b26b --- /dev/null +++ b/app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt @@ -0,0 +1,43 @@ +package dev.carlos.acronyms.views + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI +import dev.carlos.acronyms.R +import dev.carlos.acronyms.di.appModules +import dev.carlos.acronyms.viewmodel.NavigationViewmodel +import dev.carlos.core.extensions.findNavHostFragment +import dev.carlos.core.extensions.observeNonNull +import dev.carlos.core.navigation.NavigationRouter +import org.koin.android.ext.android.inject +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.context.startKoin + +class MainActivity : AppCompatActivity() { + + private val navController by lazy { findNavController(R.id.main_fragment_container) } + private val navRouter: NavigationRouter by inject() + private val viewModel: NavigationViewmodel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setupNavigation() + } + + private fun setupNavigation() { + navController.setGraph(R.navigation.main_navigation) + navRouter.setOnNavigationEvent { + viewModel.onNavigationEvent(it) + } + viewModel.navigate.observeNonNull(this) { pair -> + // Pair.first is a Navigation Id + // Pair.second is a Bundle + navController.navigate(pair.first, pair.second) + } + + NavigationUI.setupActionBarWithNavController(this, navController) + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a21b3e1 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml new file mode 100644 index 0000000..f17e2a2 --- /dev/null +++ b/app/src/main/res/navigation/main_navigation.xml @@ -0,0 +1,6 @@ + + + + diff --git a/core/src/main/java/dev/carlos/core/domain/Either.kt b/core/src/main/java/dev/carlos/core/domain/Either.kt new file mode 100644 index 0000000..f51ce75 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/domain/Either.kt @@ -0,0 +1,33 @@ +package dev.carlos.core.domain + +sealed class Either { + + data class Left(val a: L) : Either() + data class Right(val b: R) : Either() + + val isRight get() = this is Right + val isLeft get() = this is Left + + fun left(a: L) = Left(a) + fun right(b: R) = Right(b) + + fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any = + when (this) { + is Left -> fnL(a) + is Right -> fnR(b) + } +} + +fun ((A) -> B).c(f: (B) -> C): (A) -> C = { + f(this(it)) +} + +fun Either.flatMap(fn: (R) -> Either): Either = + when (this) { + is Either.Left -> Either.Left( + a + ) + is Either.Right -> fn(b) + } + +fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right)) diff --git a/core/src/main/java/dev/carlos/core/domain/network/RemoteClient.kt b/core/src/main/java/dev/carlos/core/domain/network/RemoteClient.kt new file mode 100644 index 0000000..ae41e79 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/domain/network/RemoteClient.kt @@ -0,0 +1,36 @@ +package dev.carlos.core.domain.network + +import dev.carlos.core.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import timber.log.Timber +import java.util.concurrent.TimeUnit + +private const val TIMEOUT_IN_SECONDS = 60L + +class RemoteClient(endpoint: String) { + private val logger = HttpLoggingInterceptor { message -> Timber.d(message) }.setLevel(getLoggerLevel()) + + private val client = OkHttpClient.Builder() + .addInterceptor(logger) + .readTimeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) + .connectTimeout(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) + .build() + + private val retrofit: Retrofit = Retrofit.Builder() + .baseUrl(endpoint) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(client) + .build() + + fun getClient(api: Class): T = retrofit.create(api) + + private fun getLoggerLevel() = when (BuildConfig.DEBUG) { + true -> HttpLoggingInterceptor.Level.BASIC + false -> HttpLoggingInterceptor.Level.NONE + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt b/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt new file mode 100644 index 0000000..1c8cafc --- /dev/null +++ b/core/src/main/java/dev/carlos/core/domain/network/RequestStatus.kt @@ -0,0 +1,8 @@ +package dev.carlos.core.domain.network + +sealed class RequestStatus { + object Ready : RequestStatus() + object Loading : RequestStatus() + object Errored : RequestStatus() + object Empty : RequestStatus() +} diff --git a/core/src/main/java/dev/carlos/core/extensions/ApplicationExtension.kt b/core/src/main/java/dev/carlos/core/extensions/ApplicationExtension.kt new file mode 100644 index 0000000..be1050e --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/ApplicationExtension.kt @@ -0,0 +1,8 @@ +package dev.carlos.core.extensions + +import android.content.Context +import android.widget.Toast + +fun Context.toast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} diff --git a/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt new file mode 100644 index 0000000..1176e23 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/DataExtensions.kt @@ -0,0 +1,15 @@ +package dev.carlos.core.extensions + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +fun LiveData.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) { + observe(owner, Observer { + it?.let { + func(it) + } + }) +} + +fun Int.wasUpdated() = this > 0 diff --git a/core/src/main/java/dev/carlos/core/extensions/NavigationExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/NavigationExtensions.kt new file mode 100644 index 0000000..97025f0 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/NavigationExtensions.kt @@ -0,0 +1,32 @@ +package dev.carlos.core.extensions + +import android.os.Bundle +import android.view.View +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.navigation.fragment.NavHostFragment + +fun AppCompatActivity.findNavHostFragment(@IdRes id: Int) = + supportFragmentManager.findFragmentById(id) as NavHostFragment + +fun NavHostFragment.registerOnFragmentViewCreated( + recursive: Boolean = true, + listener: (currentFragment: Fragment) -> Unit +) { + childFragmentManager + .registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentViewCreated( + fm: FragmentManager, + f: Fragment, + v: View, + savedInstanceState: Bundle? + ) { + super.onFragmentViewCreated(fm, f, v, savedInstanceState) + listener(f) + } + }, recursive) +} + +fun buildNavigation(@IdRes id: Int, bundle: Bundle = Bundle()) = Pair(id, bundle) diff --git a/core/src/main/java/dev/carlos/core/extensions/ViewExtensions.kt b/core/src/main/java/dev/carlos/core/extensions/ViewExtensions.kt new file mode 100644 index 0000000..3241e1f --- /dev/null +++ b/core/src/main/java/dev/carlos/core/extensions/ViewExtensions.kt @@ -0,0 +1,60 @@ +package dev.carlos.core.extensions + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView + +fun View.enable() { + isEnabled = true +} + +fun View.disable() { + isEnabled = false +} + +fun View.visible() { + visibility = View.VISIBLE +} + +fun View.invisible() { + visibility = View.INVISIBLE +} + +fun View.transparent() { + alpha = 0f +} + +fun View.opaque() { + alpha = 1f +} + +fun View.gone() { + visibility = View.GONE +} + +fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View = + LayoutInflater + .from(context) + .inflate(layout, this, attachToRoot) + +fun RecyclerView.Adapter<*>.autoNotify(oldList: List, newList: List, compare: (T, T) -> Boolean) { + val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return compare(oldList[oldItemPosition], newList[newItemPosition]) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + }) + + diff.dispatchUpdatesTo(this) +} diff --git a/core/src/main/java/dev/carlos/core/navigation/NavigationEvent.kt b/core/src/main/java/dev/carlos/core/navigation/NavigationEvent.kt new file mode 100644 index 0000000..d6f0f83 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/navigation/NavigationEvent.kt @@ -0,0 +1,7 @@ +package dev.carlos.core.navigation + +interface NavigationEvent + +interface NavigationController { + fun sendNavigation(event: NavigationEvent) +} diff --git a/core/src/main/java/dev/carlos/core/navigation/NavigationRouter.kt b/core/src/main/java/dev/carlos/core/navigation/NavigationRouter.kt new file mode 100644 index 0000000..638a009 --- /dev/null +++ b/core/src/main/java/dev/carlos/core/navigation/NavigationRouter.kt @@ -0,0 +1,13 @@ +package dev.carlos.core.navigation + +class NavigationRouter : NavigationController { + private var onNavigationEvent: (NavigationEvent) -> Unit = {} + + override fun sendNavigation(event: NavigationEvent) { + onNavigationEvent(event) + } + + fun setOnNavigationEvent(listener: (NavigationEvent) -> Unit) { + onNavigationEvent = listener + } +}