Android login/logout flow with the new SplashScreen API - android

I'm using the new SplashScreen API for android 12 but I'm a bit confused on the login flow now. I have one activity and multiple fragments as advised by google , the mainActivity is where the splashScreen launches and the user's supposed to be directed to either login fragment or the homefragment.
my question is how do I implement this work flow with the new SplashAPI? What fragment should be the startDestination? I don't want to use the popTo attribute since it doesn't look pretty to always show the loginfragment and then direct the user to Homefragment.
If someone could explain this to me I'd be grateful.

Homefragment should be the startDestination. Conditionally navigate to loginfragment and pop back to Homefragment after authentication.
Refer to the following video by Ian Lake.
https://www.youtube.com/watch?v=09qjn706ITA

I have a workaround for that you can set the content of the activity after you check if the user is authorized or not and save the user state in a ViewModel or something, then set your content upon this state.
I leverage the power of setKeepOnScreenCondition function from core-splashscreen library.
SplashInstaller.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SplashInstaller(
activity = this,
beforeHide = {
// Check if logged in or not asyncrounsly
delay(2000)
},
afterHide = {
setContent()
// start with you desired destination up on the check you did in beforeHide
})
}
/**
* #author Mohamed Elshaarawy on Oct 14, 2021.
*/
class SplashInstaller<A : ComponentActivity>(
private val activity: A,
visibilityPredicate: () -> Boolean = { BuildConfig.BUILD_TYPE != "debug" },
private val beforeHide: suspend A.() -> Unit = { delay(2000) },
private val afterHide: A.() -> Unit
) : CoroutineScope by activity.lifecycleScope {
private val isSplashVisibleChannel by lazy { Channel<Boolean>() }
private val isAfterCalled by lazy { Channel<Boolean>(capacity = 1) }
private val splashSuspensionJob = launch(start = CoroutineStart.LAZY) {
activity.beforeHide()
isSplashVisibleChannel.send(false)
}
init {
if (visibilityPredicate()) {
splashSuspensionJob.start()
installSplash()
} else afterSplash()
}
private fun installSplash() {
activity.installSplashScreen().setKeepOnScreenCondition {
val isVisible = isSplashVisibleChannel.tryReceive().getOrNull() ?: true
if (!isVisible) {
afterSplash()
}
isVisible
}
}
private fun afterSplash() {
if (isAfterCalled.tryReceive().getOrNull() != true) {
isAfterCalled.trySend(true)
activity.afterHide()
}
}
}
This solution uses
androidx.core:core-splashscreen:1.0.0-beta01
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2

Related

Why does Flow (kotlinx.coroutines.flow) not working with Retry even though I manually set as null in Android?

So basically, on the snackbar action button, I want to Retry API call if user click on Retry.
I have used core MVVM architecture with Flow. I even used Flow between Viewmodel and view as well. Please note that I was already using livedata between view and ViewModel, but now the requirement has been changed and I have to use Flow only. Also I'm not using and shared or state flow, that is not required.
Code:
Fragment:
private fun apiCall() {
viewModel.fetchUserReviewData()
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.userReviewData?.collect {
LogUtils.d("Hello it: " + it.code)
setLoadingState(it.state)
when (it.status) {
Resource.Status.ERROR -> showErrorSnackBarLayout(-1, it.message, {
// Retry action button logic
viewModel.userReviewData = null
apiCall()
})
}
}
}
Viewmodel:
var userReviewData: Flow<Resource<ReviewResponse>>? = emptyFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
userReviewData = flow {
emit(Resource.loading(true))
repository.getUserReviewData().collect {
emit(it)
}
}
}
EDIT in ViewModel:
// var userReviewData = MutableStateFlow<Resource<ReviewResponse>>(Resource.loading(false))
var userReviewData = MutableSharedFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
viewModelScope.launch {
userReviewData.emit(Resource.loading(true))
repository.getUserReviewData().collect {
userReviewData.emit(it)
}
}
}
override fun onCreate() {}
}
EDIT in Activity:
private fun setObservers() {
lifecycleScope.launchWhenStarted {
viewModel.userReviewData.collect {
setLoadingState(it.state)
when (it.status) {
Resource.Status.SUCCESS ->
if (it.data != null) {
val reviewResponse: ReviewResponse = it.data
if (!AppUtils.isNull(reviewResponse)) {
setReviewData(reviewResponse.data)
}
}
Resource.Status.ERROR -> showErrorSnackBarLayout(it.code, it.message) {
viewModel.fetchUserReviewData()
}
}
}
}
}
Now, I have only single doubt, should I use state one or shared one? I saw Phillip Lackener video and understood the difference, but still thinking what to use!
The thing is we only support Portrait orientation, but what in future requirement comes? In that case I think I have to use state one so that it can survive configuration changes! Don't know what to do!
Because of the single responsibility principle, the ViewModel alone should be updating its flow to show the latest requested data, rather than having to cancel the ongoing request and resubscribe to a new one from the Fragment side.
Here is one way you could do it. Use a MutableSharedFlow for triggering fetch requests and flatMapLatest to restart the downstream flow on a new request.
A Channel could also be used as a trigger, but it's a little more concise with MutableSharedFlow.
//In ViewModel
private val fetchRequest = MutableSharedFlow<Unit>(replay = 1, BufferOverflow.DROP_OLDEST)
var userReviewData = fetchRequest.flatMapLatest {
flow {
emit(Resource.loading(true))
emitAll(repository.getUserReviewData())
}
}.shareIn(viewModelScope, SharingStarted.WhlieSubscribed(5000), 1)
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
fetchRequest.tryEmit(Unit)
}
Your existing Fragment code above should work with this, but you no longer need the ?. null-safe call since the flow is not nullable.
However, if the coroutine does anything to views, you should use viewLifecycle.lifecycleScope instead of just lifecycleScope.

Kotlin KMM stop coroutine flow with infinite loop properly

I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?
I designed the app like follows:
ViewModel (separate for Android and iOS)
UseCase (shared)
Repository (shared)
Data source (shared)
Android Jetpack compose single activity
iOS SwiftUI
Android ViewModel:(iOS use ObservableObject, but logic is the same)
#HiltViewModel
class NewsViewModel #Inject constructor(
private val startFetchingNews: GetNewsUseCase,
private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
private val _mutableNewsUiState = MutableStateFlow(NewsState())
val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()
fun onTriggerEvent(action: MapEvents) {
when (action) {
is NewsEvent.GetNews -> {
getNews()
}
is MapEvents.StopNews -> {
//????
}
else -> {
}
}
}
private fun getNews()() {
startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
}
UseCase:
class GetNewsUseCase(
private val newsRepo: NewsRepoInterface) {
companion object {
private val UPDATE_INTERVAL = 30.seconds
}
operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
while (true) {
emit(Result.loading())
val result = newsRepo.getNews()
if (result.succeeded) {
// emit result
} else {
//emit error
}
delay(UPDATE_INTERVAL)
}
}.asCommonFlow()
}
Repository:
class NewsRepository(
private val sourceNews: SourceNews,
private val cacheNews: CacheNews) : NewsRepoInterface {
override suspend fun getNews(): Result<List<News>> {
val news = sourceNews.fetchNews()
//.....
cacheNews.insert(news) //could be a lot of news
return Result.data(cacheNews.selectAll())
}
}
Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
) {
onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}
I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!
When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.
I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.
fun <T> Flow<T>.collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
): Job {
return onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
// ...
private var fetchNewsJob: Job? = null
private fun getNews()() {
fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.

Why is the collect of a flow in a nested fragment (ViewModel) not called?

I have a setup where an Activity holds two fragments (A, B), and B has a ViewPager with 3 Fragments (B1, B2, B3)
In the activity (ViewModel) I observe a model (Model) from Room, and publish the results to a local shared flow.
val mSharedFlow = MutableSharedFlow<Model>` that I publish the model updates to:
...
viewModelScope.launch { repo.observeModel().collect(sharedFlow) }
Fragments A and B (ViewModels) have access to the sharedFlow through (Parent) fun getModelFlow(): Flow<Model>
There are no problems running the collects for each Fragment:
viewModelScope.launch {
parent.getModelFlow().collect { model -> doStuff(model) }
}
But, the problem is in the nested fragments (B1 etc.)
In the fragment (ViewModel) for B1 I have another parent.getModelFlow() that in turn calls Fragment B (ViewModel) parent.getParentFlow()
I have no problem acquiring the flow (i.e the SharedFlow (as Flow from the activity ViewModel)); But the collect in B1 does nothing.
Why am I not able to collect from the shared flow in the nested B1? (When A and B works fine)
Am I already not taking some flow rules into consideration? Additional launch{}'es, other withContexts(Some?), flowOn, launchIn etc.?
(The providing of the flow is not the problem. Even if I create intermediary flows, or place the sharedFlow in a kotlin Singleton object I still have the same problem)
=== EDIT ===
I was asked to add more information, unfortunately (for all) I can't paste the actual code because it would just appear verbose and foreign (see my comment below). But here's some psuedo-code that should be equivalent.
One clarification, that you can see below, Activity, FragmentA, FragmentB, FragmentB1 (etc.) are all running at the same time- but only one of A and B is visible at one time.
class TheActivity {
fun onCreate() {
setupFragments()
}
/** Two fragments active at the same time,
but only one FrameLayout is visible at one time */
private fun setupFragments() {
val a = FragmentA.newInstance()
val b = FragmentB.newInstance()
supportFragmentManager.commit {
add(R.id.fragment_holder_a, a)
add(R.id.fragment_holder_b, b)
}
}
}
class ActivityViewModel {
val activityModelFlow = MutableSharedFlow<Model>()
fun start() {
viewModelScope.launch {
getRoomFlow(id).collect(activityModelFlow)
}
}
}
class FragmentA { // Not very interesting
val viewModel: ViewModelA
}
class ViewModelA {
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("A model: $model")
}
}
}
}
class FragmentB {
val viewModel: ViewModelB
val viewPagerAdapter = object : PagesAdapter.Pages {
override val count: Int = 1
override fun title(position: Int): String = "B1"
override fun render(position: Int): Fragment = FragmentB1.newInstance()
}
}
class ViewModelB {
val bModelFlow: Flow<Model> get() = parentViewModel.activityModelFlow
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("B model: $model")
}
}
}
}
class Fragment B1 {
val viewModel: ViewModelB1
}
class ViewModelB1 {
fun start() {
viewModelScope.launch {
// in essence:
// - ViewModelB.bModelFlow ->
// - ActivityViewModel.modelFlow
parentViewModel.bModelFlow.collect { model ->
log("B1 model: $model")
}
}
}
}
So, all of the connections of acquiring parentViewModels, DI, fragment creation etc. is all working fine. But B1 model: $model is never called! Why?
This had very little (read no) connection to fragments, lifecycle, flows and coroutines blocking - which I thought was behind this.
val activityModelFlow = MutableSharedFlow<Model>()
// is the same as
val activityModelFlow = MutableSharedFlow<Model>(
replay = 0,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
This means that new subscribers will get access to 0(!) of the values that are stored in the replay cache. So when FragmentB1 gets around to subscribing, the model has already been emitted.
Solution (without any optimisation or further consideration)
private val bulletFlow = MutableSharedFlow<Bullet>(replay = 1)
(or use a StateFlow, but I don't want to bother with initial state/value)

Refresh in Repository get called every time onCreate() of Activity called ( not in screen rotation )

I have following project in Github : https://github.com/AliRezaeiii/TVMaze
I have started to using Koin as dependency injection framework in a sample app :
class TVMazeApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#TVMazeApplication)
modules(networkModule)
modules(persistenceModule)
modules(repositoryModule)
modules(viewModelModule)
}
}
}
This is my repository class :
class ShowRepository(
private val dao: ShowDao,
private val api: TVMazeService,
private val context: Context
) {
/**
* A list of shows that can be shown on the screen.
*/
val shows = resultLiveData(
databaseQuery = {
Transformations.map(dao.getShows()) {
it.asDomainModel()
}
},
networkCall = { refreshShows() })
/**
* Refresh the shows stored in the offline cache.
*/
private suspend fun refreshShows(): Result<List<Show>> =
try {
if (isNetworkAvailable(context)) {
val shows = api.fetchShowList().await()
dao.insertAll(*shows.asDatabaseModel())
Result.success(shows)
} else {
Result.error(context.getString(R.string.failed_internet_msg))
}
} catch (err: HttpException) {
Result.error(context.getString(R.string.failed_loading_msg))
}
}
And my ViewModel :
class MainViewModel(
repository: ShowRepository
) : ViewModel() {
private val _shows = repository.shows
val shows: LiveData<Result<List<Show>>>
get() = _shows
}
And I observe LiveData in my Activity :
viewModel.shows.observe(this, Observer { result ->
when (result.status) {
Result.Status.SUCCESS -> {
binding.loadingSpinner.hide()
viewModelAdapter.submitList(result.data)
}
Result.Status.LOADING -> binding.loadingSpinner.show()
Result.Status.ERROR -> {
binding.loadingSpinner.hide()
Snackbar.make(binding.root, result.message!!, Snackbar.LENGTH_LONG).show()
}
}
})
When I click on Back button, Activity get destroyed ( but instance of app still exist as I can access it from recent apps). What I expect is a call to refreshShows() method when I start the app again, but it never get called.
But when I destroy instance of app by clearing from recent app and start the app, refreshShows() get called.
What should I do to have a call on refreshShows() every time onCreate() callback of Activity get called?
fun <T, A> resultLiveData(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Result<A>): LiveData<Result<T>> =
liveData(Dispatchers.IO) {
emit(Result.loading<T>())
val source = databaseQuery.invoke().map { Result.success(it) }
emitSource(source)
val result = networkCall.invoke()
if (result.status == Result.Status.ERROR) {
emit(Result.error<T>(result.message!!))
emitSource(source)
}
}
Your refreshShows() in your repository is only get called when a new network request is done. The idea of your livedata is to provide the latest result when its fragment/activity is recreated, so when your screen rotates or you resume an activity it doesnt triggers another request as the livedata already have the latest result and you dont have a stateful connection with your network database/server (if you were observing data from Room it would receive the latest change if any).
The simpliest way I find to "fix" this, is to actually have your viewmodel val shows to be a fun, like this:
class MainViewModel(
repository: ShowRepository
) : ViewModel() {
private val _shows = repository.shows()
val shows: LiveData<Result<List<Show>>>
get() = _shows
}
However using like this, everytime the screen rotates a new network call will be made thus calling your refreshShows()

Making stateful components in Android

I am using MVVM in my app. When you enter a query and click search button, the chain is as follows: Fragment -> ViewModel -> Repository -> API -> Client. The client is where HTTP requests are made. But there is one thing here, the client needs to make a call and get a key from the server at initialization. Therefore, to prevent any call before it this first call completes, I need to be able to observe it from Fragment so that I can disable search button. Since each component in the chain can communicate with adjacent components, all components should have a state.
I am thinking to implement a StatefulComponent class and make all components to extend it:
open class StatefulComponent protected constructor() {
enum class State {
CREATED, LOADING, LOADED, FAILED
}
private val currentState = MutableLiveData(State.CREATED)
fun setState(newState: State) {
currentState.value = newState
}
val state: LiveData<State> = currentState
val isLoaded: Boolean = currentState.value == State.LOADED
val isFailed: Boolean = currentState.value == State.FAILED
val isCompleted: Boolean = isLoaded || isFailed
}
The idea is that each component observers the next one and updates itself accordingly. However, this is not possible for ViewModel since it is already extending ViewModel super class.
How can I implement a solution for this problem?
The most common approach is to use sealed class as your state, so you have any paramaters as you want on each state case.
sealed class MyState {
object Loading : MyState()
data class Loaded(data: Data) : MyState()
data class Failed(message: String) : MyState()
}
On your viewmodel you will have only 1 livedata
class MyViewModel : ViewModel() {
private val _state = MutableLiveData<MyState>()
val state: LiveData<MyState> = _state
fun load() {
_state.postCall(Loading)
repo.loadSomeData(onData = { data ->
_state.postCall(Loaded(data))
}, onError = { error -> _state.postCall(Failed(error.message)) })
}
// coroutines approach
suspend fun loadSuspend() {
_state.postCall(Loading)
try {
_state.postCall(Loaded(repo.loadSomeDataSupend()))
} catch(e: Exception) {
_state.postCall(Failed(e.message))
}
}
}
And on the fragment, just observe the state
class MyFragment : Fragment() {
...
onViewCreated() {
viewModel.state.observer(Observer {
when (state) {
// auto casts to each state
Loading -> { button.isEnabled = false }
is Loaded -> { ... }
is Failed -> { ... }
}
}
)
}
}
As João Gouveia mentioned, we can make stateful components quite easily using kotlin's sealed classes.
But to make it further more useful, we can introduce Generics! So, our state class becomes StatefulData<T> which you can use pretty much anywhere (LiveData, Flows, or even in Callbacks).
sealed class StatefulData<out T : Any> {
data class Success<T : Any>(val result : T) : StatefulData<T>()
data class Error(val msg : String) : StatefulData<Nothing>()
object Loading : StatefulData<Nothing>()
}
I've wrote an article fully explaining this particular implementation here
https://naingaungluu.medium.com/stateful-data-on-android-with-sealed-classes-and-kotlin-flow-33e2537ccf55
If you are using the composable ... You can use produce state
#Composable
fun PokemonDetailScreen(
viewModel: PokemonDetailVm = hiltViewModel()
) {
/**
* This takes a initial state and with that we get a coroutine scope where we can call a API and assign the data into the value
*/
val pokemonInfo = produceState<Resource<Pokemon>>(initialValue = Resource.Loading()) {
value = viewModel.getPokemonInfo(pokemonName)
}.value
}

Categories

Resources