mirror of
https://github.com/imcarlost/Acronyms.git
synced 2026-04-09 18:38:28 -04:00
Add: Main app, main activity and navigation handling
This commit is contained in:
@@ -3,13 +3,32 @@
|
||||
package="dev.carlos.acronyms"
|
||||
>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:supportsRtl="false"
|
||||
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>
|
||||
|
||||
32
app/src/main/java/dev/carlos/acronyms/MainApplication.kt
Normal file
32
app/src/main/java/dev/carlos/acronyms/MainApplication.kt
Normal 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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/dev/carlos/acronyms/di/AppModules.kt
Normal file
17
app/src/main/java/dev/carlos/acronyms/di/AppModules.kt
Normal 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() }
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt
Normal file
43
app/src/main/java/dev/carlos/acronyms/views/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
20
app/src/main/res/layout/activity_main.xml
Normal file
20
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
6
app/src/main/res/navigation/main_navigation.xml
Normal file
6
app/src/main/res/navigation/main_navigation.xml
Normal 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>
|
||||
33
core/src/main/java/dev/carlos/core/domain/Either.kt
Normal file
33
core/src/main/java/dev/carlos/core/domain/Either.kt
Normal 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))
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.carlos.core.navigation
|
||||
|
||||
interface NavigationEvent
|
||||
|
||||
interface NavigationController {
|
||||
fun sendNavigation(event: NavigationEvent)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user