Add: Main app, main activity and navigation handling

This commit is contained in:
Carlos Martinez
2021-06-15 22:08:27 -04:00
parent e85196ad03
commit 7a62f260ee
16 changed files with 370 additions and 3 deletions

View File

@@ -3,13 +3,32 @@
package="dev.carlos.acronyms" package="dev.carlos.acronyms"
> >
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:label="@string/app_name" android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="false"
android:theme="@style/Theme.Acronyms" android:theme="@style/Theme.Acronyms"
/> android:hardwareAccelerated="true"
>
<activity
android:name=".views.MainActivity"
android:launchMode="singleTask"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest> </manifest>

View File

@@ -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
)
)
}
}
}

View File

@@ -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() }
}

View File

@@ -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<Pair<@IdRes Int, Bundle>>()
fun onNavigationEvent(event: NavigationEvent) {
when (event) {
else -> throw NoWhenBranchMatchedException("Undefined navigation event parent")
}
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_navigation"
>
</navigation>

View File

@@ -0,0 +1,33 @@
package dev.carlos.core.domain
sealed class Either<out L, out R> {
data class Left<out L>(val a: L) : Either<L, Nothing>()
data class Right<out R>(val b: R) : Either<Nothing, R>()
val isRight get() = this is Right<R>
val isLeft get() = this is Left<L>
fun <L> left(a: L) = Left(a)
fun <R> 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> ((A) -> B).c(f: (B) -> C): (A) -> C = {
f(this(it))
}
fun <T, L, R> Either<L, R>.flatMap(fn: (R) -> Either<L, T>): Either<L, T> =
when (this) {
is Either.Left -> Either.Left(
a
)
is Either.Right -> fn(b)
}
fun <T, L, R> Either<L, R>.map(fn: (R) -> (T)): Either<L, T> = this.flatMap(fn.c(::right))

View File

@@ -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 <T> getClient(api: Class<T>): T = retrofit.create(api)
private fun getLoggerLevel() = when (BuildConfig.DEBUG) {
true -> HttpLoggingInterceptor.Level.BASIC
false -> HttpLoggingInterceptor.Level.NONE
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -0,0 +1,15 @@
package dev.carlos.core.extensions
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, func: (T) -> Unit) {
observe(owner, Observer {
it?.let {
func(it)
}
})
}
fun Int.wasUpdated() = this > 0

View File

@@ -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)

View File

@@ -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 <T> RecyclerView.Adapter<*>.autoNotify(oldList: List<T>, newList: List<T>, 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)
}

View File

@@ -0,0 +1,7 @@
package dev.carlos.core.navigation
interface NavigationEvent
interface NavigationController {
fun sendNavigation(event: NavigationEvent)
}

View File

@@ -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
}
}