How to make Fragments to be stateful in an Android Activity - android

I'm a newbie in Android.
I am trying to figure out how to create a stateful fragment based on MVVM with a SavedStateHandle injection.
I have created an empty project in Android Studio based on a template with bottom navigation. Then I added a text input into DashboardFragment. I want the text of this input to be saved when user switches between tabs.
At the moment I get access to VM as follows:
private val application = activity?.application
private val viewModel: DashboardViewModel by viewModels {
SavedStateViewModelFactory(application, activity as SavedStateRegistryOwner)
}
And I get an exception
ava.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
Please, help me to find out what is wrong
Source code: https://github.com/ideogram-software/statefulfragments-tests/

The problem was in androidx.navigation:navigation-fragment-ktx and androidx.navigation:navigation-ui-ktx dependencies version. They were 2.3.5.
I have changed them as follows (thanks to #ianhanniballake)
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01'
and the problem has gone

Related

Adding a custom class breaks Compose compilation

I'm just trying to get started with Android development and Kotlin using Jetpack Compose. Note that, I'm a Kotlin novice, so I'm trying to learn along the way. I come from JavaScript/TypeScript background, so I'm trying to learn by thinking in JavaScript terms and implement in Kotlin terms by searching online.
I'm trying to list all installed applications on the device. While the app was working as expected up till now, I needed to add a feature to sort the installed app names. I referred to: https://www.bezkoder.com/kotlin-sort-list-objects/#Create_Class_for_handling_sorting. As soon as I added a custom class to sort the List<ApplicationInfo>, my app stopped building.
I have included my repo here: https://github.com/Hrishikesh-K/TryKotlin
If I comment these lines and this line as well, the app builds fine. With the current setup, I get an error:
Functions which invoke #Composable functions must be marked with the #Composable annotation
which points to line 21, character 18, which is the start of the word compare.
I don't understand why Compose would care about a custom class, it's not a Composable function after all. What am I missing?
In the compare method you are using LocalContext.current
override fun compare(o1 : ApplicationInfo, o2 : ApplicationInfo): Int {
return o1.loadLabel(LocalContext.current.packageManager).toString().compareTo(o2.loadLabel(LocalContext.current.packageManager).toString())
}
You can't use a #Composable functions if the method is not marked with the #Composable.
Use something different like:
data class CompareApplicationNames(val context: Context) : Comparator<ApplicationInfo> {
override fun compare(o1 : ApplicationInfo, o2 : ApplicationInfo): Int {
return o1.loadLabel(context.packageManager).toString().compareTo(o2.loadLabel(context.packageManager).toString())
}
}
Then just use:
Log.d("sorted:", listOfApplications.sortedWith(CompareApplicationNames(LocalContext.current)).toString())

Room-based code is causing the following error: android.app.Application cannot be cast to com.example.mathreps.MathRepsApplication

I am working on adding data persistence to an app using the Room library, based on this documentation. My database only has one column where a String corresponding to a user-selected radio button is to be placed. However, as soon as I attempt to insert a new object (in this case, an Attempt), it gives me the following error:
java.lang.ClassCastException: android.app.Application cannot be cast to com.example.mathreps.MathRepsApplication.
I believe this error comes from the following block of code within my Rating fragment:
private val dataViewModel: MathRepsViewModel by activityViewModels {
MathRepsViewModel.MathRepsViewModelFactory(
(activity?.application as MathRepsApplication).database
.attemptDao()
)
}
Which relates to a different class called MathRepsApplication
class MathRepsApplication: Application() {
val database: AttemptRoomDatabase by lazy {AttemptRoomDatabase.getDatabase(this)}
}
Since this problem most likely relates to multiple classes and packages, here is the link to the repository for the project.
My attempts to fix this have been unsuccessful, with them mostly consisting of heavily reviewing the database implementation and not much else, as I don't exactly know where to start.

Jetpack Compose Preview not working when using Koin for Dependency Injection

I want to use Jetpack Compose in my App. I am already using Koin for DI. Because I have a lot of convenience methods in my BaseFragment I want to inherit from it and build the corresponding view with compose.
Now the Problem is that when using DI in the BaseFragment and inheriting from it the preview of the composable wont be shown and following error Message appears:
and following exception is thrown:
java.lang.IllegalStateException: KoinApplication has not been started
at org.koin.core.context.GlobalContext.get(GlobalContext.kt:36)
at org.koin.java.KoinJavaComponent.getKoin(KoinJavaComponent.kt:122)
at org.koin.java.KoinJavaComponent.get(KoinJavaComponent.kt:87)
at org.koin.java.KoinJavaComponent.get$default(KoinJavaComponent.kt:81)
at org.koin.java.KoinJavaComponent.get(KoinJavaComponent.kt)
...
My BaseFragment looks something like this
public abstract class BaseFragment {
private final ActiveViewIdInteractor activeViewIdInteractor =
new ActiveViewIdInteractor(KoinJavaComponent.get(ActiveViewIdService.class));
...
and my Fragment which inherits looks something like this
class ComposeDemoFragment: BaseFragment() {
...
#Composable
fun ComposeDemoFragmentContent() {
Text(text = "Hello World",
Modifier
.fillMaxWidth()
.background(Color.Cyan)
)
}
#Preview
#Composable
private fun Preview() {
ComposeDemoFragmentContent()
}
If using the exact same preview in a Fragment which doesn't inherit from BaseFragment everything works fine. I already included the dependency for "Koin for Compose" and also tried using CoKoin. At this Point I don't know what to do with the error Message or if the error Message is even barely related to the actual Problem.
Is this a Bug or is there a way to bypass this error?
Your #Preview code is being run as-is by Android Studio, looking at your example there is nothing in your ComposeDemoFragmentContent() that is using Koin. However, I'm guessing this is just sample code.
In my app we inject koin components into our main PrimaryTheme{ } which used to break when used with #Preview, we got the same error as you're seeing.
One way around this is to provide a default value to the field being injected, and then put your koin code inside a check for LocalInspectionMode e.g.
val someField = remember { mutableStateOf("Default")}
if (!LocalInspectionMode.current) {
// We're _not_ executing in an Android Studio Preview.
// Use your injected Koin instance here e.g.
val myUseCase: CustomUseCase = get()
someField.value = myUseCase.getSomeValue()
}
Text(
text = someField.value
)
So your previews will use the default value, but your real app will use the koin injected value.
This happens because your #Preview function is inside your Activity. And you probably have an injected member there.
Move it to the root of the file, outside the activity class, and the preview will be rendered without errors.

Instrumented Testing with Dagger2 - how to reference fake repository?

My goal is to test an app with Espresso.
First Screen Activity depends on settings received from a Repository. Repository checks whether the user saved a location preference in Shared Preferences. If he has, it moves on to the Main Activity. That's the part of logic that I am trying to test.
I want to substitute fake repository (HashMap representing shared preferences) to achieve consistency. Tests run and pass if the repository is empty (base state). However, I want to test whether the app moves forward if the location is saved.
Test in question:
#Test
fun onLaunch_withLocationSaved_checkMainActivityIsShown() {
fakeRepository.saveLocation("40,80")
ActivityScenario.launch(FirstScreenActivity::class.java)
onView(withText(R.string.welcome_message)).check(matches(not(isDisplayed())))
}
How do I get a reference to fakeRepository to be able to save location that ViewModel will read from?
If it's created (which defeats the point of injection) like this:
#Before
fun init() {
fakeRepository = FakeSimpleRepository()
fakeRepository.saveLocation("")
viewModel = FirstScreenViewModel(fakeRepository)
}
The viewmodel gets injected with a different fakeRepository object (I compared addresses with a debugger).
I followed Google's codelabs and official documentation how to set up Dagger with my app. Their examples do not show how to reference the repository though to make changes.
This blog cover what you want to achieve
The idea is that you have to override your dagger module to inject the mock object. Then you create a custom runner class to override the application class.

After updating MvvmCross 5.2 I have error Fragment already active

I have a problem after updating to the new MvvmCross 5.2.
I have forced uninstalled MvvmCross.Droid.Shared and after update all packages. I then got some errors with MvxFragment, so I replaced it with MvxFragmentPresentation. Additionally, I replaced MvxCachingFragmentCompatActivity with MvxAppCompatActivity and I'm now using the new MvxAppCompatViewPresenter. All works well, and the app running good. Except after I select logout in menu I'm taken to the LoginViewModel and when I want Login again I get this error
Fragment already active.
Can someone help me?
My test project is HERE on github.
It fail here, by the ShowViewModel
public class MainViewModel : BaseViewModel
{
public void ShowMenu()
{
ShowViewModel<MenuViewModel>();
}
}
The issue is that you are mixing your methods for presenting in MvvmCross. With MvvmCross 5.x a new prefer way to navigate was introduced using the IMvxNavigationService. For new apps it is suggested that you rather make use of the IMvxNavigationService over the prior ShowViewModel. It is advised that you do not mix the use of the two different ways to navigate as you may get some strange behaviour.
Switching to that IMvxNavigationService which you are already using on the LoginViewModel will solve the exception you are getting.
protected readonly IMvxNavigationService _mvxNavigationService;
public MainViewModel(IMvxNavigationService mvxNavigationService)
{
_mvxNavigationService = mvxNavigationService;
}
public void ShowMenu()
{
_mvxNavigationService.Navigate<MenuViewModel>();
}
Additionally, you will want to remove adding HomeFragment to the backstack to prevent seeing a white page when navigation back.
[MvxFragmentPresentation(typeof(MainViewModel), Resource.Id.content_frame)]
public class HomeFragment : BaseFragment<HomeViewModel>
See pull request for full details of changes.
Additional notes
Rather than explicitly specifying MvxAppCompatViewPresenter in your Setup which inherits MvxAndroidSetup you can rather inherit from MvxAppCompatSetup which will automatically make use of the MvxAppCompatViewPresenter as well as register additional AndroidViewAssemblies relating to support libraries (see link to which assemblies) and FillTargetFactories for the MvxAppCompatSetupHelper.

Categories

Resources