feature TI-195: [Android] Cicirone Navigation

This commit is contained in:
Evgeny Dubravin 2024-03-29 22:41:40 +07:00
parent c4b740f629
commit d71fda5853
15 changed files with 305 additions and 8 deletions

View File

@ -88,6 +88,9 @@ dependencies {
// Groupie
implementation(libs.groupie)
implementation(libs.groupie.viewbinding)
// Cicecrone
implementation(libs.cicerone)
}
apply(from = "${rootProject.ext["buildScriptsDir"]}/gradle/scripts/stringGenerator.gradle")

View File

@ -1,6 +0,0 @@
package ru.touchin.template
import androidx.lifecycle.ViewModel
import javax.inject.Inject
class SingleViewModel @Inject constructor() : ViewModel()

View File

@ -2,14 +2,22 @@ package ru.touchin.template.base.fragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import ru.touchin.template.base.viewmodel.BaseController
import ru.touchin.template.di.SharedComponent
import ru.touchin.template.di.getSharedModule
import ru.touchin.template.navigation.backpress.OnBackPressedListener
abstract class BaseFragment<T : ViewModel> : Fragment() {
abstract class BaseFragment<T : ViewModel> : Fragment(), OnBackPressedListener {
protected val viewModel: T by lazy { createViewModelLazy().value }
protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() }
protected abstract fun createViewModelLazy(): Lazy<T>
protected fun getSharedComponent(): SharedComponent = getSharedModule()
override fun onBackPressed(): Boolean {
return (viewModel as BaseController).onBackClicked()
}
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.base.viewmodel
interface BaseController {
fun onBackClicked(): Boolean = true
}

View File

@ -6,12 +6,15 @@ import dagger.Component
import javax.inject.Singleton
import ru.touchin.template.App
import ru.touchin.template.di.modules.AppModule
import ru.touchin.template.di.modules.NavigationModule
import ru.touchin.template.di.modules.ViewModelModule
import ru.touchin.template.feature.SingleActivity
@Component(
modules = [
AppModule::class,
ViewModelModule::class
ViewModelModule::class,
NavigationModule::class
]
)
@Singleton
@ -27,4 +30,6 @@ interface AppComponent : SharedComponent {
}
fun inject(entry: App)
fun inject(entry: SingleActivity)
}

View File

@ -0,0 +1,26 @@
package ru.touchin.template.di.modules
import com.github.terrakok.cicerone.Cicerone
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Router
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class NavigationModule {
private val cicerone: Cicerone<Router> = Cicerone.create()
@Provides
@Singleton
fun provideRouter(): Router {
return cicerone.router
}
@Provides
@Singleton
fun provideNavigatorHolder(): NavigatorHolder {
return cicerone.getNavigatorHolder()
}
}

View File

@ -0,0 +1,47 @@
package ru.touchin.template.feature
import android.os.Bundle
import androidx.activity.viewModels
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.androidx.AppNavigator
import javax.inject.Inject
import ru.touchin.template.R
import ru.touchin.template.base.activity.BaseActivity
import ru.touchin.template.databinding.ActivityMainBinding
import ru.touchin.template.di.DI
class SingleActivity : BaseActivity<SingleViewModel>() {
@Inject
lateinit var navigatorHolder: NavigatorHolder
private val rootNavigator by lazy {
AppNavigator(this, R.id.activityScreensContainer)
}
private lateinit var binding: ActivityMainBinding
override fun createViewModelLazy(): Lazy<SingleViewModel> = viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
DI.getComponent().inject(this)
viewModel.navigate()
}
override fun onResumeFragments() {
super.onResumeFragments()
navigatorHolder.setNavigator(rootNavigator)
}
override fun onPause() {
navigatorHolder.removeNavigator()
super.onPause()
}
}

View File

@ -0,0 +1,14 @@
package ru.touchin.template.feature
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import javax.inject.Inject
import ru.touchin.template.navigation.Screens
class SingleViewModel @Inject constructor(
private val router: Router,
) : ViewModel() {
fun navigate() {
router.newRootScreen(Screens.First())
}
}

View File

@ -0,0 +1,41 @@
package ru.touchin.template.feature.first
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import ru.touchin.template.base.fragment.BaseFragment
import ru.touchin.template.databinding.FragmentFirstBinding
class FirstFragment : BaseFragment<FirstViewModel>() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
override fun createViewModelLazy() = viewModels<FirstViewModel> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFirstBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
button.apply {
text = "Next"
setOnClickListener { viewModel.onNextButtonClicked(this@FirstFragment::class.toString()) }
}
textView.text = "First Fragment"
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,15 @@
package ru.touchin.template.feature.first
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import javax.inject.Inject
import ru.touchin.template.navigation.Screens
class FirstViewModel @Inject constructor(
private val router: Router
) : ViewModel() {
fun onNextButtonClicked(from: String) {
router.navigateTo(Screens.Second(from))
}
}

View File

@ -0,0 +1,75 @@
package ru.touchin.template.feature.second
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import ru.touchin.template.base.fragment.BaseFragment
import ru.touchin.template.databinding.FragmentSecondBinding
import ru.touchin.template.di.viewmodel.assistedViewModel
class SecondFragment : BaseFragment<SecondViewModel>() {
companion object {
private const val FROM_KEY = "FROM"
private const val SCREEN_NAME_KEY = "SCREEN_NAME"
private const val FRAGMENT_NAME = "Second Fragment"
fun newInstance(from: String): SecondFragment {
val args = bundleOf(FROM_KEY to from, SCREEN_NAME_KEY to FRAGMENT_NAME)
val fragment = SecondFragment().apply {
arguments = args
}
return fragment
}
}
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val secondViewModelFactory by lazy { getSharedComponent().secondScreenViewModelFactory() }
override fun createViewModelLazy() = assistedViewModel {
secondViewModelFactory.create(
arguments?.getString(FROM_KEY) ?: "Unkown Fragment",
arguments?.getString(SCREEN_NAME_KEY) ?: "Unknown"
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSecondBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
button.apply {
text = "Back"
setOnClickListener { onBackPressed() }
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect {
textView.text = it
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,34 @@
package ru.touchin.template.feature.second
import androidx.lifecycle.ViewModel
import com.github.terrakok.cicerone.Router
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import ru.touchin.template.base.viewmodel.BaseController
class SecondViewModel @AssistedInject constructor(
@Assisted("from") from: String,
@Assisted("screenName") screenName: String,
private val rootRouter: Router
) : ViewModel(), BaseController {
private val _state = MutableStateFlow("$from to $screenName")
val state: StateFlow<String> = _state.asStateFlow()
@AssistedFactory
interface Factory {
fun create(
@Assisted("from") from: String,
@Assisted("screenName") screenName: String
): SecondViewModel
}
override fun onBackClicked(): Boolean {
rootRouter.exit()
return super.onBackClicked()
}
}

View File

@ -0,0 +1,16 @@
package ru.touchin.template.navigation
import com.github.terrakok.cicerone.androidx.FragmentScreen
import ru.touchin.template.feature.first.FirstFragment
import ru.touchin.template.feature.second.SecondFragment
object Screens {
fun First(): FragmentScreen = FragmentScreen {
FirstFragment()
}
fun Second(from: String): FragmentScreen = FragmentScreen {
SecondFragment.newInstance(from)
}
}

View File

@ -0,0 +1,6 @@
package ru.touchin.template.navigation.backpress
interface OnBackPressedListener {
fun onBackPressed(): Boolean
}

View File

@ -0,0 +1,7 @@
package ru.touchin.template.navigation.router
import com.github.terrakok.cicerone.Router
interface RouterProvider {
val router: Router
}