avatarILLUMINATION

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

13719

Abstract

"hljs-keyword">else</span> <span class="hljs-number">1</span> - (item.offset.toFloat() + item.size) / (layoutInfo.viewportSize.height + item.size) }

<span class="hljs-keyword">else</span> -&gt; <span class="hljs-number">1f</span>

}</pre></div><p id="2309">Then for each element that gets animated; I used its scroll progress as a driver for its animation. This allowed me to create custom animations for elements when they scroll in and out of the screen.</p><div id="bda3"><pre><span class="hljs-comment">// Using progress to drive animation</span> <span class="hljs-keyword">val</span> parallaxProgress <span class="hljs-keyword">by</span> remember { derivedStateOf { scrollState.scrollProgressFor(<span class="hljs-number">11</span>) } } ParallaxImages( parallaxProgress = parallaxProgress, topImagePath = wonder.photo3, bottomImagePath = wonder.photo4, )</pre></div><h1 id="4dc3">MapView — Integrating platform-specific views</h1><figure id="0eb3"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*EsBzMKMPEbI3RwkI7aOHCQ.png"><figcaption></figcaption></figure><p id="4bfd">There is currently no out-of-box solution for displaying Maps in compose multiplatform. Being relatively new, Compose multiplatform doesn’t enjoy the library ecosystem as large as Flutter. But that might not even be needed… thanks to Koltin Multiplatform.</p><p id="37bf">Kotlin multiplatform (KMP) is the technology that Compose Multiplatform itself is built upon. Generally, writing code that can interoperate with platform APIs is a difficult task. In Flutter for example; you have to create MethodChanels, which requires you to make the caller function asynchronous. You also need to pass the data in the form of primitive types. You also need to ensure you are naming the methods right on all the target platforms. KMP makes it a breeze to share code between multiple platforms while providing platform-specific implementations. It becomes easy to create your own small multiplatform library using KMP.</p><p id="85f4">This example of MapView demonstrates how platform-specific implementation can be created. It implements a cross-platform MapView using. It is not as good as a true cross-platform library from a single vendor, but it’s a good starting point.</p><ul><li>Google Maps library for Android</li><li>MKMapView for iOS</li><li>Map module extracted from the official sample for JVM desktop</li><li>OpenLayers javascript library for Web</li></ul><p id="6271">In short, you write an <code>expect</code>ed function signature in the common source set, and in the platform-specific source set, you provide an <code>actual</code> implementation. This is similar(not the same) to how you define an interface and then provide its implementation.</p><div id="9a71"><pre><span class="hljs-comment">// Function with "expect" modifier has no body</span> <span class="hljs-meta">@Composable</span> <span class="hljs-keyword">expect</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">MapView</span><span class="hljs-params">( modifier: <span class="hljs-type">Modifier</span>, gps: <span class="hljs-type">GpsPosition</span>, title: <span class="hljs-type">String</span>, parentScrollEnableState: <span class="hljs-type">MutableState</span><<span class="hljs-type">Boolean</span>> = mutableStateOf(<span class="hljs-literal">true</span>)</span></span>, zoomLevel: <span class="hljs-built_in">Float</span> = <span class="hljs-number">10f</span>, mapType: MapType = MapType.Normal, )</pre></div><p id="b39e">This is the function that will be used in the common UI code. But the Kotlin compiler won’t be happy till we provide <code>actual</code> implementations of this function for all the platforms.</p><p id="92ac">For Android, the actual implementation uses <code>GoogleMap</code> from Maps Compose Library. This library itself uses the View-based Google Map library internally. Compose provides an interoperability layer in the form of <code>AndroidView</code> that allows us to use platform-specific UI elements.</p><div id="f4ff"><pre><span class="hljs-comment">// In Android module the actual implementation is provided</span> <span class="hljs-meta">@OptIn(ExperimentalComposeUiApi::class)</span> <span class="hljs-meta">@Composable</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">MapView</span><span class="hljs-params">( modifier: <span class="hljs-type">Modifier</span>, gps: <span class="hljs-type">GpsPosition</span>, title: <span class="hljs-type">String</span>, parentScrollEnableState: <span class="hljs-type">MutableState</span><<span class="hljs-type">Boolean</span>>, zoomLevel: <span class="hljs-type">Float</span>, mapType: <span class="hljs-type">MapType</span> )</span></span> { <span class="hljs-comment">// ....</span> <span class="hljs-comment">// We can embed native Android View here via AndroidView</span> GoogleMap( modifier = modifier, cameraPositionState = cameraPositionState, properties = MapProperties(mapType = mapType.toGoogleMapType()) ) { <span class="hljs-keyword">val</span> markerState = rememberMarkerState(position = currentLocation) Marker(markerState) } }</pre></div><p id="e792">Here in IOS implementation, you can see Compose’s interoperability in action. <code>UIKitView</code> is a composable function that allows embedding UIKit elements inside Compose. The <code>MKMapView</code> is not a Compose function; it’s a native UIKit view written in Objective-C; called from Kotlin 🤯.</p><div id="aeab"><pre><span class="hljs-comment">// actual IOS implementation</span> <span class="hljs-meta">@OptIn(ExperimentalForeignApi::class)</span> <span class="hljs-meta">@Composable</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">MapView</span><span class="hljs-params">( modifier: <span class="hljs-type">Modifier</span>, gps: <span class="hljs-type">GpsPosition</span>, title: <span class="hljs-type">String</span>, parentScrollEnableState: <span class="hljs-type">MutableState</span><<span class="hljs-type">Boolean</span>>, zoomLevel: <span class="hljs-type">Float</span>, mapType: <span class="hljs-type">MapType</span> )</span></span> { <span class="hljs-keyword">val</span> location = CLLocationCoordinate2DMake(gps.latitude, gps.longitude) <span class="hljs-keyword">val</span> <span class="hljs-keyword">annotation</span> = remember { MKPointAnnotation( location, title = <span class="hljs-literal">null</span>, subtitle = <span class="hljs-literal">null</span> ) } <span class="hljs-keyword">annotation</span>.setTitle(title) UIKitView( modifier = modifier, factory = { <span class="hljs-comment">// Initializing MKMapView</span> MKMapView().apply { addAnnotation(<span class="hljs-keyword">annotation</span>) setMapType(mapType.toAppleMapType()) } }, update = { it.addAnnotations(listOf(MKPointAnnotation(location))) it.setRegion( MKCoordinateRegionMakeWithDistance( centerCoordinate = location, zoomLevel * <span class="hljs-number">10_000.0</span>, zoomLevel * <span class="hljs-number">10_000.0</span> ), animated = <span class="hljs-literal">false</span> ) } ) }

<span class="hljs-function"><span class="hljs-keyword">fun</span> MapType.<span class="hljs-title">toAppleMapType</span><span class="hljs-params">()</span></span> = <span class="hljs-keyword">when</span> (<span class="hljs-keyword">this</span>) { MapType.Normal -> MKMapTypeStandard MapType.Satellite -> MKMapTypeSatellite }</pre></div><p id="96f8">For the desktop platform, Compose once again provides interoperability with Swing via <code>SwingPanel</code> composable. Since unfortunately I couldn’t find any Swing-based map viewer; I chose to copy <code>mapview-desktop</code> module which is a pure Compose based module from official samples.</p><p id="5745">For web targets, Compose for now doesn’t provide any out-of-box solution for embedding HTML content inside composables. Here I’ve used a <a href="https://github.com/Hamamas/Kotlin-Wasm-Html-Interop">Kotlin-Wasm-Html-Interop</a> by Soufiane Hamama to embed <a href="https://openlayers.org/">OpenLayers</a> map.</p><div id="f4bb"><pre><span class="hljs-comment">// Actual WASM implementation</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">MapView</span><span class="hljs-params">( modifier: <span class="hljs-type">Modifier</span>, latLng: <span class="hljs-type">LatLng</span>, title: <span class="hljs-type">String</span>, parentScrollEnableState: <span class="hljs-type">MutableState</span><<span class="hljs-type">Boolean</span>>, zoomLevel: <span class="hljs-type">Float</span>, mapType: <span class="hljs-type">MapType</span> )</span></span> { <span class="hljs-keyword">val</span> map = remember { openlayersmap.Map() } <span class="hljs-keyword">val</span> adjustedZoomLevel = zoomLevel * <span class="hljs-number">5f</span>

<span class="hljs-comment">// https://github.com/Hamamas/Kotlin-Wasm-Html-Interop</span>
HtmlView(
    modifier = modifier,
    factory = {
        createElement(<span class="hljs-string">"div"</span>).apply {
            id = <span class="hljs-string">"map"</span>
        }
    },
    update = {
        <span class="hljs-comment">// Calling OpenLayers Javascript code from here</span>
        <span class="hljs-keyword">val</span> center = fromLonLat(LonLat(latLng.longitude, latLng.latitude))
        <span class="hljs-keyword">val</span> tileLayer = TileLayer(TileLayerOptions(source = mapType.toOlSource()))
        <span class="hljs-keyword">val</span> vectorLayerForMaker = Vector(
            VectorOptions(
                source = VectorSource(VectorSourceOptions(feature = Feature(Point(center)))),
                style = DEFAULT_MARKER_STYLE,
            ),
        )
        <span class="hljs-keyword">val</span> view = View(
            ViewOptions(
                center = center,
                zoom = adjustedZoomLevel,
                maxZoom = MAX_ZOOM
            )
        )
        map.apply {
            setTarget(<span class="hljs-string">"map"</span>)
            setLayers(jsArrayOf(tileLayer, vectorLayerForMaker))
            setView(view)
        }
    },
)

}</pre></div><h1 id="fd5f">Photo Gallery — Custom Layout and Gesture detection</h1><p id="357c">In this screen; instead of using built-in <code>LazyVerticalGridView</code> which supports scrolling in only one direction; I created a custom grid layout. This <code>SimpleGrid</code> Layout simply puts its children in a grid :P</p><div id="f7fa"><pre><span class="hljs-meta">@Composable</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">SimpleGrid</span><span class="hljs-params">( modifier: <span class="hljs-type">Modifier</span> = Modifier, columnCount: <span class="hljs-type">Int</span>, content: @<span class="hljs-type">Composable</span> () -> <span class="hljs-type">Unit</span>, )</span></span> { Layout( modifier = modifier, content = content ) { measurables, constraints ->

    <span class="hljs-keyword">val</span> placeables = measurables.map { it.measure(constraints) }

    layout(constraints.maxWidth, constraints.maxHeight) {
        <span class="hljs-keyword">var</span> x = <span class="hljs-number">0</span>
        <span class="hljs-keyword">var</span> y = <span class="hljs-number">0</span>
        placeables.chunked(columnCount).forEach { placeables -&gt;
            placeables.forEach { placeable -&gt;
                <span class="hljs-comment">// placing in a row</span>
                placeable.place(x, y)
                x += placeable.width
            }
            x = <span class="hljs-number">0</span> <span class="hljs-comment">// reset x</span>
            y += placeables.maxOf { it.height } <span class="hljs-comment">// move to next row</span>
        }
    }
}

}</pre></div><p id="34d4">You can see here how easy it is to create custom layouts in Compose.</p><ul><li>First, call <code>Layout</code> function providing it the Composable content you want to lay out.</li><li>In the callback, the first parameter we receive is the measurables which is the list of actual UI elements (children) that will be placed in our layout. The second parameter is the constraints provided by the parent.</li><li>By measuring the measurables to our constraints we receive a list of placeables.</li><li>And then finally we place those placeables by providing their x and y coordinates.</li></ul><figure id="6266"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*Kq2zdNjP9Iv6nYxbQroV0Q.png"><figcaption></figcaption></figure><p id="9b2b">Here for example; the items are placed in a row by incrementing their x values by their widths; then for the next row x value is reset to 0 and the y value is incre

Options

mented by the height of the tallest element in that row. The placement is relative to top-left corner of the element. You could add item spacing logic pretty easily, but I handled it by applying padding around individual items.</p><h2 id="d204">Eight-Way Gesture Detector</h2><p id="7209">The tricky part of this screen was gesture recognition. To allow users to swipe in any of the 8 directions a custom modifier was created. This Modifier detects swipes in 8 directions and sends the normalized vector representing the direction.</p><div id="4e8c"><pre><span class="hljs-comment">/**

  • the [onSwipe] callback receives normalized direction.

  • (-1, -1) left-top

  • (1, 1) right-bottom */</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">eightWaySwipeDetector</span><span class="hljs-params">( minDragLength: <span class="hljs-type">Int</span> = <span class="hljs-number">20</span>, onSwipe: (<span class="hljs-type">direction</span>: <span class="hljs-type">Offset</span>) -> <span class="hljs-type">Unit</span>, )</span></span> = composed { <span class="hljs-keyword">var</span> offset: Offset = Offset.Zero pointerInput(<span class="hljs-built_in">Unit</span>) { detectDragGestures( onDragEnd = { <span class="hljs-keyword">if</span> (abs(offset.x) > minDragLength || abs(offset.y) > minDragLength) { <span class="hljs-keyword">val</span> normalizedOffset = offset.normalizedDirection() <span class="hljs-keyword">if</span> (normalizedOffset != Offset.Zero) onSwipe(normalizedOffset) } offset = Offset.Zero }, ) { change, dragAmount -> change.consume() offset += dragAmount } } }</pre></div><p id="35c8">This uses lower-level <code>pointerInput</code> APIs to get drag amount using <code>detectDragGesture</code> . This drag is accumulated until it ends. If the accumulated drag is larger than <code>minSwipeLenght</code>; its normalized value (swipe direction) is sent through the callback, else it is ignored.</p><h1 id="77c2">Artifact Details Screen — Logic in UI layer</h1><p id="79a3">This screen has some data-fetching logic. It fetches artifact data from the Metropolitan Museum of Art. Since the amount of logic here is minimal; I’ve chosen to keep it in the Composable function itself. Ideally, a ViewModel should be used here; but I’ve kept it for demonstration purposes.</p><p id="e98c">First, the repository (which handles communication logic) is instantiated in a<code>remember</code> call. As compose may re-execute the Composable functions to react to the state changes; our repository will be created again and again. Wrapping it in <code>remember</code> is the way to keep an instance of it in the memory.</p><div id="766c"><pre><span class="hljs-meta">@Composable</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ArtifactDetailsScreen</span><span class="hljs-params">( artifactId: <span class="hljs-type">String</span>, onClickBack: () -> <span class="hljs-type">Unit</span>, )</span></span> { <span class="hljs-comment">// This is not an ideal way to handle data fetching. Use ViewModel instead.</span> <span class="hljs-keyword">val</span> repo = remember { MetRepository() } <span class="hljs-keyword">var</span> result <span class="hljs-keyword">by</span> remember { mutableStateOf<Result<ArtifactData?>?>(<span class="hljs-literal">null</span>) } LaunchedEffect(artifactId) { <span class="hljs-comment">// This will only run once, when the artifactId changes</span> result = repo.getArtifactById(artifactId) }

    <span class="hljs-comment">// UI</span>

}</pre></div><p id="8f65">Then to store the result of the API call; I created another <code>remember</code>ed variable called <code>result</code>. The result itself is wrapped in a <code>MutableState</code> . This is the way to notify the Compose framework that when this value gets updated; UI also needs to be updated. Finally to trigger the API call; <code>LaunchedEffect</code> with key <code>artifactId</code> is used. This may look similar to <code>useEffect</code> from React. The <code>artifcatId</code> key provided ensures <code>LaunchedEffect</code> only re-runs our API function when the user selects a different artifact.</p><h1 id="5b7f">Artifact List Screen — Logic in ViewModel (MVVM)</h1><p id="4e01">This screen shows all the artifacts for a wonder and allows searching them. As this screen contains some business logic; a <a href="https://readmedium.com/viewmodels-a-simple-example-ed5ac416317e">ViewModel</a> is used here.</p><blockquote id="027f"><p>ViewModel is the businees logic holder that survives recompositions and configuration changes (Android). It is supposed to be free from framework or platform dependencies.</p></blockquote><p id="b2e9">The <code>ArtifactListViewModel</code> receives the current wonder as input and then it exposes some observable State properties and functions to mutate those properties.</p><p id="d3c9">For example here the ViewModel exposes <code>searchText</code>, <code>suggestions</code>, and <code>filteredArtifacts</code> states and an <code>onSearch</code> and <code>onQueryChange</code> functions to the UI. UI observes the states and displays the UI. When the user performs any action; it invokes the functions which in turn update the states.</p><div id="b091"><pre><span class="hljs-keyword">class</span> <span class="hljs-title class_">ArtifactListViewModel</span>(wonder: Wonder) : ViewModel() {

<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> allSuggestions = wonder.searchSuggestions
<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> allArtifacts = wonder.searchData

<span class="hljs-keyword">val</span> searchText = MutableStateFlow(<span class="hljs-string">""</span>)

<span class="hljs-keyword">val</span> suggestions = searchText.map {
    <span class="hljs-keyword">if</span> (it.isBlank()) allSuggestions
    <span class="hljs-keyword">else</span> allSuggestions.filter { suggestion -&gt;
        suggestion.contains(it.lowercase())
    }
}.stateIn(
    viewModelScope,
    SharingStarted.WhileSubscribed(<span class="hljs-number">5000</span>),
    allSuggestions
)


<span class="hljs-keyword">val</span> filteredArtifacts = MutableStateFlow(allArtifacts)

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onQueryChange</span><span class="hljs-params">(query: <span class="hljs-type">String</span>)</span></span> = searchText.update { query }

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onSearch</span><span class="hljs-params">(suggestion: <span class="hljs-type">String</span>)</span></span> {
    filteredArtifacts.update {
        allArtifacts.filter { artifact -&gt;
            artifact.title.lowercase().contains(suggestion) ||
                    artifact.keywords.lowercase().contains(suggestion)
        }
    }
}

}</pre></div><h1 id="308d">Wonders Timeline — Logic in the UI controller</h1><figure id="116a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*IFu-2N3C3-JPZ1PRXFGIww.gif"><figcaption></figcaption></figure><p id="dca2">This screen presents a timeline of all the important events and the construction period of all the wonders. This screen displays the current year based on how far the user has scrolled and also animates to a certain location based on selected Wonder. I thought it would be nice if I could abstract away this logic. This was a perfect opportunity to create a UI state-holder named <code>TimelineState</code> .</p><p id="b9fa"><code>TimelineState</code> class is a view state holder class that simply wraps a <code>ScrollState</code> and exposes its methods and properties in terms of years instead of pixels. For example, <code>ScrollState</code> has <code>scrollTo</code> method which requires pixel offset for scrolling. <code>TimelineState</code> exposes this as <code>scrollToYear</code> which requires a target year to which you want to scroll.</p><blockquote id="17f8"><p>Compose uses <i>State</i> classes to abstract/store UI components state. Such as TextFieldState. In Flutter world this kind of class is generally called a <i>Controller such as </i>TextEditingController. Besides naming and how observable data is exposed or retained; there’s not much difference here.</p></blockquote><div id="78b4"><pre><span class="hljs-keyword">class</span> <span class="hljs-title class_">TimelineState</span>(initialScroll: <span class="hljs-built_in">Int</span> = <span class="hljs-number">0</span>) { <span class="hljs-keyword">val</span> scrollState = ScrollState(initialScroll) <span class="hljs-keyword">var</span> scale <span class="hljs-keyword">by</span> mutableStateOf(<span class="hljs-number">1.</span>dp) <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>

<span class="hljs-keyword">val</span> timelineHeight <span class="hljs-keyword">get</span>() = TimelineDuration * scale

<span class="hljs-keyword">val</span> scrollFraction <span class="hljs-keyword">by</span> derivedStateOf {
    ((scrollState.value) / scrollState.maxValue.toFloat()).coerceIn(<span class="hljs-number">0f</span>, <span class="hljs-number">1f</span>)
}

<span class="hljs-keyword">val</span> currentYear <span class="hljs-keyword">by</span> derivedStateOf {
    (scrollFraction * TimelineDuration - <span class="hljs-number">3000</span>).roundToInt()
}

<span class="hljs-keyword">val</span> currentYearHighlightRange <span class="hljs-keyword">get</span>() = currentYear.eventHighlightRange

<span class="hljs-keyword">val</span> currentTimelineEvent <span class="hljs-keyword">by</span> derivedStateOf {
    AllTimelineEvents.firstOrNull { it.year <span class="hljs-keyword">in</span> currentYearHighlightRange }
}

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setScale</span><span class="hljs-params">(zoom: <span class="hljs-type">Float</span>)</span></span> {
    scale = (scale * zoom).coerceIn(MinZoomScale, MaxZoomScale)
}

<span class="hljs-comment">/**
 * Scrolls to the [year] starting from 1000 years above it.
 */</span>
<span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">animateRevealYear</span><span class="hljs-params">(year: <span class="hljs-type">Int</span>)</span></span> {
    <span class="hljs-keyword">val</span> startYear = (year - <span class="hljs-number">1000</span>).coerceAtLeast(StartYear)
    <span class="hljs-comment">// first snap timeline to scroll pos just above the selected year</span>
    scrollToYear(startYear)
    <span class="hljs-comment">// then animate from that point till year appears on screen</span>
    animateScrollToYear(
        year, spring(
            dampingRatio = Spring.DampingRatioLowBouncy,
            stiffness = <span class="hljs-number">25f</span>
        )
    )
}

<span class="hljs-comment">/// Private methods excluded for brevity</span>

<span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
    <span class="hljs-comment">/**
     * This is used to save [TimelineState] in an event of Activity or Process recreation
     */</span>
    <span class="hljs-keyword">val</span> Saver = Saver&lt;TimelineState, <span class="hljs-built_in">Int</span>&gt;(
        save = { it.scrollState.value },
        restore = { TimelineState(it) }
    )
}

}</pre></div><p id="2b05">The Saver object here allows Compose to save and restore this class in case of Process recreation.</p><p id="58ee">The scrolling timeline section is wrapped in a scrollable Box composable (A Box composable with <code>scrollable</code> modifier applied). The time markers are in a <code>LazyColumn</code>. While the rest of the screen uses some combination of box/column/row for the placement.</p><h1 id="c888">Wonder Events Screen — Adaptive Layout</h1><figure id="912a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*NQRTFCCRV76rbIyMyDlRbQ.png"><figcaption>Adaptive Layout in Wodner Events Details scren</figcaption></figure><p id="2bb6">The Wonder Events screen is made responsive for phones and tablets. Two layouts are used to achieve this. For screens lesser than 600 dp width, a single-column layout is used. Wonder image is in the body of the <code>BottomSheetScaffold</code> while; The list of events is placed inside a <code>BottomSheetScaffold</code>‘s <code>sheetContent</code>. For screens larger than 600 dp a <code>Row</code> is used to achieve a two-column layout.</p><h1 id="f2b2">What’s Next</h1><p id="c6ea">As mentioned earlier, there’s still a lot of work to do. These are some of the goals.</p><ul><li>Add Collectibles and My Collection screen.</li><li>System independent localization.</li><li>Add documentation and tests.</li></ul><p id="7cba">Thanks to the team <a href="https://gskinner.com/">gskinner</a> for creating the original Wonderous. Without it, this app wouldn’t have been possible. All the assets used in the app are taken from their repository. Do check out the <a href="https://flutter.gskinner.com/wonderous/">original app</a>.</p></article></body>

Meet Aamir Kamal

Senior editor at ILLUMINATION Integrated Publications on Medium

Photo courtesy of Aamir Kamal

This post introduces Aamir Kamal 🚀🚀🚀, a senior and consulting editor of Illumination Integrated Publications on Medium.

Aamir is an Industrial Engineer by profession. Living in Pakistan, he started writing online because he needed an income as a graduate.

His first online presence happened by borrowing his brother’s computer to post his writing to the Internet. His family’s financial background wasn’t very good, so he decided to take various paying jobs during school holidays like teaching science subjects to students and vaccinating people against Polio Virus in his country.

Aamir learned how to make money by writing online. He loved the process of creating content on eclectic topics in diverse fields. His first blog was on Blogspot, also known as Blogger.com.

Unfortunately, he didn’t have the money to purchase a custom domain and pay for hosting. Writing on Blogger saved him from financial problems. However, later, he discovered that a sub-domain has no SEO potential compared to a custom domain run on WordPress with fast hosting.

Aamir started to earn income through YouTube. But it ended badly. After two years of struggle during his college days, he started making good money using different techniques. So, he began reading more books about SEO, Google AdSense, niche blogging, professional writing, and blogging in general.

He joined Medium in 2020. One of the best things that happened to him after his blog made him over $20,000 in one year. He started regularly publishing on Medium. He is grateful that Medium lets him self-publish his engaging and informative content.

In addition to supporting Illumination Integrated Publications as an editor, he also established his own publication on Medium, called Menlo Blogging. His publication serves bloggers who want to earn income through online writing. Aamir is also active on Vocal Media and Quora.

We selected a few informative articles from his vast writing portfolio.

Medium SEO: How to rank your Medium articles in Google search to get evergreen traffic?

The process I used to rank a blog post on Google search

How to make the most of the AHREFs $7 Trail for One week

15 Creative ways to make money from your WordPress blog

I started a blog with $0 and Made over $15,000

Is Medium a good place for personal blogging?

What are some sites like Medium for making money writing?

Why Substack is a better Medium alternative?

How to make money writing on Vocal Media?

How to be productive as a writer?

Thank you for reading Aamir’s stories. He looks forward to connecting with his readers on Medium and several other platforms.

Other Editor Profiles

You find profiles of our editors as collected and curated by ILLUMINATION-Curated in this following collection.

Writer Biographies

Profiles of our writers from their pen.

Featured Stories by Editors

Please enjoy stories featured by our editors.

Publication Resources

All publication related stories can be accessed via this collection.

Invitation to Potential Writers

To join our vibrant publications, please send a request via this link. We will help you gain visibility and succeed as a writer on Medium. Please point out the publication name with your Medium account ID in the request. If you are new to Medium, you can join via this link. Readers can read thousands of stories and writers can monetize self-published content.

Writing
Reading
Editing
Entrepreneurship
Blogger
Recommended from ReadMedium