Jetpack Compose - Processing after completion of composition [duplicate] - android-jetpack-compose

I'm currently studying Jetpack Compose in an attempt to build a feature-rich application using modern Android architecture components. Traditionally, each screen (or navigation unit) in my application would be either an activity or a fragment, each with its own lifecycle bindings, but with Jetpack Compose and the Compose Navigation library, I would do something like this:
MainActivity.kt:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
// More composable calls
}
}
}
}
Where MainScreen is just a composable. My questions are:
What is the equivalent here of a "lifecycle" for this composable? Say I want to perform some action when the screen is loaded, when it is destroyed etc. This is perhaps more relevant to the case where I have more screens and navigation between them
Is there some way to integrate between Compose and standard activities? That is, define activities for screens as you would, with each activity being a ComponentActivity and defining its own composable layout? Is this discouraged for some reason?

The Compose application is designed to be used in a single-activity architecture with no fragments.
You can still have multiple activities or fragments and use setContent in each of them, but in this case the transfer of data between activities falls on your shoulders. Use this approach if you're adding new Compose screens to an existing application built the old way.
But with Compose, it's much easier to do all the navigation within a single activity using Compose Navigation. Much less code, better performance due to no unnecessary code layers, easy to transfer data, etc.
To work with the view lifecycle, check out compose side-effects:
LaunchedEffect can be used to execute an action when the view appears. It also runs on a coroutine context that is bound to the current composable: you can easily run suspend functions, and when the view disappears from view hierarchy - the coroutine will be canceled.
DisposableEffect can be used to subscribe to/unsubscribe from callbacks.
When you rotate the screen, all effects will restart no matter which key you passed.
#Composable
fun MainScreen(navController: NavController) {
LaunchedEffect(Unit) {
println("LaunchedEffect: entered main")
var i = 0
// Just an example of coroutines usage
// don't use this way to track screen disappearance
// DisposableEffect is better for this
try {
while (true) {
delay(1000)
println("LaunchedEffect: ${i++} sec passed")
}
} catch (cancel: CancellationException) {
println("LaunchedEffect: job cancelled")
}
}
DisposableEffect(Unit) {
println("DisposableEffect: entered main")
onDispose {
println("DisposableEffect: exited main")
}
}
}
Also note that in both cases, and in many other cases in compose, you pass key to these functions. This helps compose understand when the value should be recomputed. In my example it is Unit, which means that it won't change until the view is gone. But if you create a remember value, use another dynamic value from the view model, or pass another argument to composable, you can pass it as a key, this will cancel the current LaunchedEffect job and call onDispose for DisposableEffect, and your job will be restarted with the updated key value. You can pass as many keys as you want.
Read more about the state in Compose in documentation.

Related

Jetpack Compose: LocalComposition.current

I'm looking at the NowInAndroid app, Google's Jetpack Compose sample app. In MainActivity, a calculateWindowSize method is called, which invokes LocalConfiguration.current.
What is this LocalConfiguration.current call for? We're not using the return value.
#ExperimentalMaterial3WindowSizeClassApi
#Composable
fun calculateWindowSizeClass(activity: Activity): WindowSizeClass {
// Observe view configuration changes and recalculate the size class on each change. We can't
// use Activity#onConfigurationChanged as this will sometimes fail to be called on different
// API levels, hence why this function needs to be #Composable so we can observe the
// ComposeView's configuration changes.
LocalConfiguration.current // WHAT DOES THIS DO?
val density = LocalDensity.current
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
return WindowSizeClass.calculateFromSize(size)
}
The comment above the call is trying to explain why this is done.
Just reading the value of LocalConfiguration.current will ensure this method is invoked whenever the configuration changes. The code below this call uses this indirect notification to calculate the new window size class based on the new configuration. WindowMetricsCalculator is not directly observable as it is independent of Compose. The result of calling calculateFromSize() could change whenever the configuration changes so the call to LocalConfiguration.current ensure this method is called whenever the result could change.

React Native IOS Bridge Native View - proper way to call Native Method from JS side

I want to discuss the options i have come across for calling a native method from a IOS ViewManager/View in JS side without using properties but direct method calls.
Option A:
implementing a method in the ViewManager which searches for the correct view and calls the given method in the view, like this:
func callMethodViaManager(_ node:NSNumber) {
DispatchQueue.main.async {
let myView = self.bridge.uiManager.view(forReactTag: node) as! MyView
myView.myMethod()
}
}
and then in the JS side implement a handler like this:
const handleSomething = (e) => {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(ref.current),
UIManager.SwiftComponent.Commands.callMethodViaManager,
[])
};
This is just a short summary of the relevant parts, the process in full can be seen in full detail maybe just a bit old but with some adjustments one can get it to work also with functional components:
https://medium.com/#jjdanek/react-native-calling-class-methods-on-native-swift-views-521faf44f3dc
Option B:
For this option let's go with the best scenario which is that one can get all the necessary data, setup, etc on the ViewManager ready via delegates for example or some other pattern or Swift sugar.
Calling the Native methods in the ViewManager from the JS side directly with the use of NativeModules, like this:
const handleSomething = (e) => {
NativeModules.MyViewManager.myMethod()
};
I could not find much about this option in correlation to a Native View being bridged, this way of doing it is used for Native Module bridging explicitly. The only things i could find where:
React Native UI Component Method
or guides like these one:
https://teabreak.e-spres-oh.com/swift-in-react-native-the-ultimate-guide-part-1-modules-9bb8d054db03#4377
I have tried both methods and at first glance they seem to work both.
So my questions are:
Why is Option A the better solution/option and the one which is recommended or most used?
What is wrong or can go wrong with Option B?
Is there anything else to take into consideration?
Is there a better way which is not trough properties?
Option B is more flexible.
If you use Option A, then you can't pass Promise and Callback parameter to the native side.
It seems possible in iOS But not in Android.
This is a related issue.
React Native bridge API is all confusing
There is no guide about how to call the native UI method with Promise or Callback.
There is a example about how to pass Promise to native side with calling native UI instance method.
SketchView... is just my example module name.
class SketchViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "SketchViewModule"
private fun runCommandWithPromise(
viewId: Int, promise: Promise, handler: (view: SketchView) -> Unit
) {
reactApplicationContext.getNativeModule(UIManagerModule::class.java)?.let { uiManager ->
uiManager.addUIBlock {
(it.resolveView(viewId) as? SketchView)?.let { view ->
handler(view)
} ?: run {
promise.safeReject("[runCommandWithPromise] Cannot find View with UIManager")
}
}
}
}
...

How to transform PagingData<T> into model in collectLatest() in UI

I am having a situation, where once I get pagingData <T: UIModel>, I need to get additional data from a different API. The second Api requires arguments that are there in first API response. Currently I am collecting in UI Layer in lifecyclescope as,
loadResults().collectLatest {
PagingResultAdapter.submitData(lifecycle, it)
// Extracting the data inside PagingData and setting in viewmodel.
it.map { uiModel ->
Timber.e("Getting data inside map function..")
viewModel.setFinalResults(uiModel)
}
}
}
But the problem is, the map{} function on pagingData won't run during data fetching. List is populated, ui is showing the items in recyclerview. But the map function not running..(I am not able see the log)
The UI layer loadResults() function in-turn calls the viewmodel.loadResults() with UI level variables. In terms of paging everything is working fine, but I cannot transform the pagingdata into UIModel in any layer.
Official site suggests to use map{} function only.
https://developer.android.com/topic/libraries/architecture/paging/v3-transform#basic-transformations
But I am not getting at which layer I should apply map{} and also before collecting or after collecting..Any help is good..
PagingData.map is a lazy transformation that runs during collection when you call .submitData(pagingData). Since you are only submitting the original un-transformed PagingData your .map transform will never run.
You should apply the .map to the PagingData you will actually end up submitting in order to have it run. Usually this is done from the ViewModel, so that the results are also cached in case you end up in a config change or cached scenario like when navigating between fragments.
You didn't share your ViewModel / place you are creating your Pager, but assuming this happens at a different layer you would have something like:
MyViewModel.kt
fun loadResults() = Pager(...) { ... }
.flow
.map {
Timber.e("Getting data inside map function..")
setFinalResults(uiModel)
it
}
.cachedIn(viewModelScope)
MyUi.kt
viewModel.loadResults().collectLatest {
pagingDataAdapter.submitData(it)
}
NOTE: You should use the suspending version of .submitData since you are using Flow / Coroutines, because it is able to propagate cancellation direction instead of relying on launched job + eager cancellation via the non-suspending version. There shouldn't be any visible impact, but it is more performant.
Try with:
import androidx.paging.map
.flow.map { item ->
item.map { it.yourTransformation() }
}

RxJava2 order of sequence called with compleatable andThen operator

I am trying to migrate from RxJava1 to RxJava2. I am replacing all code parts where I previously had Observable<Void> to Compleatable. However I ran into one problem with order of stream calls. When I previously was dealing with Observables and using maps and flatMaps the code worked 'as expected'. However the andthen() operator seems to work a little bit differently. Here is a sample code to simplify the problem itself.
public Single<String> getString() {
Log.d("Starting flow..")
return getCompletable().andThen(getSingle());
}
public Completable getCompletable() {
Log.d("calling getCompletable");
return Completable.create(e -> {
Log.d("doing actuall completable work");
e.onComplete();
}
);
}
public Single<String> getSingle() {
Log.d("calling getSingle");
if(conditionBasedOnActualCompletableWork) {
return getSingleA();
}else{
return getSingleB();
}
}
What I see in the logs in the end is :
1-> Log.d("Starting flow..")
2-> Log.d("calling getCompletable");
3-> Log.d("calling getSingle");
4-> Log.d("doing actuall completable work");
And as you can probably figure out I would expect line 4 to be called before line 3 (afterwards the name of andthen() operator suggest that the code would be called 'after' Completable finishes it's job). Previously I was creating the Observable<Void> using the Async.toAsync() operator and the method which is now called getSingle was in flatMap stream - it worked like I expected it to, so Log 4 would appear before 3. Now I tried changing the way the Compleatable is created - like using fromAction or fromCallable but it behaves the same. I also couldn't find any other operator to replace andthen(). To underline - the method must be a Completable since it doesn't have any thing meaning full to return - it changes the app preferences and other settings (and is used like that globally mostly working 'as expected') and those changes are needed later in the stream. I also tried to wrap getSingle() method to somehow create a Single and move the if statement inside the create block but I don't know how to use getSingleA/B() methods inside there. And I need to use them as they have their complexity of their own and it doesn't make sense to duplicate the code. Any one have any idea how to modify this in RxJava2 so it behaves the same? There are multiple places where I rely on a Compleatable job to finish before moving forward with the stream (like refreshing session token, updating db, preferences etc. - no problem in RxJava1 using flatMap).
You can use defer:
getCompletable().andThen(Single.defer(() -> getSingle()))
That way, you don't execute the contents of getSingle() immediately but only when the Completablecompletes and andThen switches to the Single.

Breeze Durandal Access shell viewmodel variable from view

I have a pretty simple thing I want to accomplish but I cannot figure out how or if it is even possible. I am using the Hot Towel template to start with. In the shell viewmodel I have a user observable. I would like to be able to reference that user observable from other pages on my site. For example from the home page. I tried a couple of things but it doenst appear as though I can access the shell from the composed view. I have a working solution at the moment that uses event pub/sub calls from the shell to pass the user data to anyone listening whenever the data changes (home view in this example). This works it just seems a little clunky and not really the ideal way to handle this. This user observable will need to be used all throughout the site to determine when certain features should be available and to show a particular users projects.
Is there a way to data bind to a knockout observable contained in the shell viewmodel from the home view?
You might consider having a global.js that returns a singleton, which you include in view models as needed.
define(function () {
return {
sharedObservable: ko.observable(),
sharedObservableArray: ko.observableArray(),
...
};
});
Using global in a viewmodel.
define([..., global], function (..., global) {
...
global.sharedObservable('updated');
// As an alternative use a local var for easier access
// var localVar = global.sharedObservable;
// localVar('updated')
...
});
The easiest way is to import the shell module into your viewmodel with requirejs and then expose the shell viewmodel as a variable on the module's viewmodel.
Somehting like this:
// Some viewmodel
define(['shell'], function(shell){
var vm = {
shell: shell,
...
};
return vm;
});
Then in your view, you can bind using $root.shell
<span data-bind="text: $root.shell.shellObservable"></span>
Non of previous answers worked for me. Thus, I tried another variant of access to shell and it made the job done.
I've got this in my shell.js:
define(['plugins/router', 'durandal/app'], function (router, app) {
return {
// I need an access to this array in my view
breadcrumbs: ko.observableArray([]),
...
};
});
And this in my view:
define(function () {
var ctor = function () {
this.pageTitle = 'Preview';
this.activate = function () {
require('viewmodels/shell').breadcrumbs([...]);
};
};
return ctor;
});
So, require('viewmodels/shell') actually returns reference to a shell object.

Resources