diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7d316a..fce5ef5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/ru/touchin/template/SingleViewModel.kt b/app/src/main/java/ru/touchin/template/SingleViewModel.kt deleted file mode 100644 index 15b24f0..0000000 --- a/app/src/main/java/ru/touchin/template/SingleViewModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.touchin.template - -import androidx.lifecycle.ViewModel -import javax.inject.Inject - -class SingleViewModel @Inject constructor() : ViewModel() \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt b/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt index 7125d9e..71d59d2 100644 --- a/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt +++ b/app/src/main/java/ru/touchin/template/base/fragment/BaseFragment.kt @@ -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 : Fragment() { +abstract class BaseFragment : Fragment(), OnBackPressedListener { + + protected val viewModel: T by lazy { createViewModelLazy().value } protected val viewModelFactory by lazy { getSharedComponent().viewModelFactory() } protected abstract fun createViewModelLazy(): Lazy protected fun getSharedComponent(): SharedComponent = getSharedModule() + + override fun onBackPressed(): Boolean { + return (viewModel as BaseController).onBackClicked() + } } \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt new file mode 100644 index 0000000..106042e --- /dev/null +++ b/app/src/main/java/ru/touchin/template/base/viewmodel/BaseController.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.base.viewmodel + +interface BaseController { + + fun onBackClicked(): Boolean = true +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/AppComponent.kt b/app/src/main/java/ru/touchin/template/di/AppComponent.kt index f4b7320..7537879 100644 --- a/app/src/main/java/ru/touchin/template/di/AppComponent.kt +++ b/app/src/main/java/ru/touchin/template/di/AppComponent.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt b/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt new file mode 100644 index 0000000..7c584b3 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/di/modules/NavigationModule.kt @@ -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 = Cicerone.create() + + @Provides + @Singleton + fun provideRouter(): Router { + return cicerone.router + } + + @Provides + @Singleton + fun provideNavigatorHolder(): NavigatorHolder { + return cicerone.getNavigatorHolder() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt b/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt new file mode 100644 index 0000000..45cbb69 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/SingleActivity.kt @@ -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() { + + @Inject + lateinit var navigatorHolder: NavigatorHolder + + private val rootNavigator by lazy { + AppNavigator(this, R.id.activityScreensContainer) + } + + private lateinit var binding: ActivityMainBinding + + override fun createViewModelLazy(): Lazy = 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() + } + +} diff --git a/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt b/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt new file mode 100644 index 0000000..394f3d5 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/SingleViewModel.kt @@ -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()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt b/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt new file mode 100644 index 0000000..ddcd6c8 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/first/FirstFragment.kt @@ -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() { + + private var _binding: FragmentFirstBinding? = null + private val binding get() = _binding!! + + override fun createViewModelLazy() = viewModels { 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt b/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt new file mode 100644 index 0000000..0b13b41 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/first/FirstViewModel.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt b/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt new file mode 100644 index 0000000..c4da57f --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/second/SecondFragment.kt @@ -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() { + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt b/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt new file mode 100644 index 0000000..d07cc14 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/feature/second/SecondViewModel.kt @@ -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 = _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() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/Screens.kt b/app/src/main/java/ru/touchin/template/navigation/Screens.kt new file mode 100644 index 0000000..51dabfd --- /dev/null +++ b/app/src/main/java/ru/touchin/template/navigation/Screens.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt b/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt new file mode 100644 index 0000000..1679240 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/navigation/backpress/OnBackPressedListener.kt @@ -0,0 +1,6 @@ +package ru.touchin.template.navigation.backpress + +interface OnBackPressedListener { + + fun onBackPressed(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt b/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt new file mode 100644 index 0000000..14426c4 --- /dev/null +++ b/app/src/main/java/ru/touchin/template/navigation/router/RouterProvider.kt @@ -0,0 +1,7 @@ +package ru.touchin.template.navigation.router + +import com.github.terrakok.cicerone.Router + +interface RouterProvider { + val router: Router +} \ No newline at end of file