<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Your awesome title</title>
    <description>A website for awesome</description>
    <link>http://g403.co/</link>
    <atom:link href="http://g403.co/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Thu, 25 Feb 2021 18:30:09 +0000</pubDate>
    <lastBuildDate>Thu, 25 Feb 2021 18:30:09 +0000</lastBuildDate>
    <generator>Jekyll v3.9.0</generator>
    
      <item>
        <title>Testing the new JetPack Compose Pt2</title>
        <description>&lt;h3 id=&quot;part-1&quot;&gt;Part 1&lt;/h3&gt;
&lt;p&gt;Look here for &lt;a href=&quot;/android-compose-testing/&quot;&gt;Part 1&lt;/a&gt; where I talk about testing loading data and UI updates from it&lt;/p&gt;

&lt;h3 id=&quot;jetpack-compose&quot;&gt;&lt;strong&gt;JetPack Compose&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In case you hadn’t noticed there’s yet another UI framework/toolkit, JetPack Compose! On the surface it has a lot more in common with Google’s Flutter than the default Android View, it’s written in code (by default Kotlin) and as such is able to generate previews a lot simpler than when you have the XML, you don’t have to bind your ViewModels to the xml, you can just pass them into the functions!&lt;/p&gt;

&lt;h3 id=&quot;testing-android-compose&quot;&gt;&lt;strong&gt;Testing Android compose&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;I want to test some basic UI interactions, we have a list in a ViewModel, clicking a fab add an item to the list and the UI updates to show the list, I won’t go through setting up the activity etc, but essentially the activity grabs the viewmodel, call setContent with the theme&amp;gt;surface&amp;gt;initial composable. The initial composeable collects the players list as state and passes it and the addPlayer function to the PlayerList composable.&lt;/p&gt;

&lt;h3 id=&quot;some-code&quot;&gt;Some code&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;class viewModel : ViewModel() {
    private val _players = MutableStateFlow(listOf&amp;lt;Player&amp;gt;())
    val players: StateFlow&amp;lt;List&amp;lt;Player&amp;gt;&amp;gt;
        get() = _players

    fun addPlayer(name: String) {
        _players.value = _players.value + Player(name)
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and our composable looks quite simple for this&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@Composable
fun PlayerList(players: List&amp;lt;Player&amp;gt;, addPlayer: (String) -&amp;gt; Unit) {
    Scaffold(floatingActionButton = {
        FloatingActionButton(
            onClick = { addPlayer(&quot;Player #${players.size}&quot;) },
            content = { Icon(Icons.Filled.Add,    &quot;Add new player&quot;) },
        )
    }) {
        ScrollableColumn(modifier = Modifier.fillMaxWidth()) {
            players.map { PlayerDisplay(player = it) }
        }
    }
}

@Composable
fun PlayerDisplay(player: Player) {
    Row() {
        Text(text = player.name, softWrap = false)
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and then in our composable test we mock the viewmodel and alter the return of the property&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;class PlayerScreensTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule&amp;lt;MainActivity&amp;gt;()

    @Test
    fun PlayerListTest() {
        composeTestRule.setContent {
            MyTheme {
                val players by composeTestRule.activity.viewModel.players.collectAsState()
                PlayerList(players, composeTestRule.activity.viewModel::addPlayer)
            }
        }

        composeTestRule.onNodeWithText(&quot;Player #0&quot;).assertDoesNotExist()
        composeTestRule.onNodeWithContentDescription(&quot;Add new player&quot;).performClick()
        composeTestRule.onNodeWithText(&quot;Player #0&quot;).assertIsDisplayed()
        composeTestRule.onNodeWithText(&quot;Player #1&quot;).assertDoesNotExist()
        composeTestRule.onNodeWithContentDescription(&quot;Add new player&quot;).performClick()
        composeTestRule.onNodeWithText(&quot;Player #0&quot;).assertIsDisplayed()
        composeTestRule.onNodeWithText(&quot;Player #1&quot;).assertIsDisplayed()
    }
}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;this all looks good, but when running this throws an error&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2 files found with path 'META-INF/AL2.0' from inputs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So now we need to update our apps build.gradle file&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;android {
    // Added to avoid this error -
    // Execution failed for task ':app:mergeDebugAndroidTestJavaResource'.
    // &amp;gt; A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
    // &amp;gt; 2 files found with path 'META-INF/AL2.0' from inputs:
    packagingOptions {
        exclude 'META-INF/AL2.0'
        exclude 'META-INF/LGPL2.1'
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;we can then run our tests successfully!&lt;/p&gt;
</description>
        <pubDate>Thu, 25 Feb 2021 00:00:00 +0000</pubDate>
        <link>http://g403.co/android-compose-testing-pt2/</link>
        <guid isPermaLink="true">http://g403.co/android-compose-testing-pt2/</guid>
        
        <category>JetPack Compose</category>
        
        <category>mockito</category>
        
        <category>Android</category>
        
        <category>Kotlin</category>
        
        
      </item>
    
      <item>
        <title>Testing the new JetPack Compose</title>
        <description>&lt;h3 id=&quot;jetpack-compose&quot;&gt;&lt;strong&gt;JetPack Compose&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;In case you hadn’t noticed there’s yet another UI framework/toolkit, JetPack Compose! On the surface it has a lot more in common with Google’s Flutter than the default Android View, it’s written in code (by default Kotlin) and as such is able to generate previews a lot simpler than when you have the XML, you don’t have to bind your ViewModels to the xml, you can just pass them into the functions!&lt;/p&gt;

&lt;h3 id=&quot;testing-android-compose&quot;&gt;&lt;strong&gt;Testing Android compose&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;There are a few good resources out there for testing JetPack’s new Compose functionality&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/testing&quot;&gt;Android Compose Testing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/Gurupreet/ComposeCookBook&quot;&gt;Gurupreet/ComposeCookBook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.android.com/jetpack/compose/navigation#testing&quot;&gt;Testing Compose Navigation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but the scenario I was struggling to test was passing ViewModel into an upper composeable, I was mocking the ViewModel but I kept getting an error&lt;/p&gt;

&lt;h3 id=&quot;some-code&quot;&gt;Some code&lt;/h3&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;open class ImageListViewModel : ViewModel() {
	private val _isLoading = MutableStateFlow(false)
	val isLoading: StateFlow&amp;lt;Boolean&amp;gt;
		get() = _isLoading
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and our composable looks quite simple for this&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@Composable
fun ImageListScreen(viewModel: ImageListViewModel) {
	val isLoading by viewModel.isLoading.collectAsState()
	val imageItems by viewModel.imageItems.collectAsState()

	if (!isLoading &amp;amp;&amp;amp; imageItems.isEmpty()) {
		Text(text = &quot;No data available&quot;)
	} else {
		Text(text = &quot;I have data&quot;)
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and then in our composable test we mock the viewmodel and alter the return of the property&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@RunWith(MockitoJUnitRunner::class)
class ImageListTest {
	@get:Rule
	val rule = createAndroidComposeRule&amp;lt;MainActivity&amp;gt;()

	@Mock
	lateinit var viewModel: ImageListViewModel

	@Test
	fun imageListScreen() {
		val isLoading = MutableStateFlow(false)
		val imageItems = MutableStateFlow(listOf&amp;lt;ImageItem&amp;gt;())

		`when`(viewModel.isLoading).thenReturn(isLoading)
		`when`(viewModel.imageItems).thenReturn(imageItems)

		rule.setContent {
			MinderaTheme(darkTheme = false) {
				ImageListScreen(viewModel = viewModel)
			}
		}

		rule.onNodeWithText(text = &quot;No data available&quot;).assertIsDisplayed()
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;but as I say, this throws an error&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This is when using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;androidTestImplementation &quot;org.mockito:mockito-android:3.5.10&quot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But the catch is that the tests for compose run on a device, not on the jvm, so when we boot the emulator and try to run the test mockito can’t work correctly as mockito uses CGLib or ByteBuddy, both of which generate .class files. When running on an Android device or emulator we need .dex files instead.&lt;/p&gt;

&lt;p&gt;We do have a couple of options, there are other mocking frameworks out there, personally I like mockito, so we go with the other option, helping mockito to run on instrumented tests.&lt;/p&gt;

&lt;h3 id=&quot;dexmaker&quot;&gt;dexmaker&lt;/h3&gt;
&lt;p&gt;We can change our dependencies a bit to make mockito work with instrumented tests&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;-- androidTestImplementation &quot;org.mockito:mockito-android:3.5.10&quot;
++ androidTestImplementation &quot;org.mockito:mockito-core:3.5.10&quot;
++ androidTestImplementation &quot;com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.0&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Dexmaker here works with mockito to generate .dex files, and since mockito inline can work with private/closed/final classes/functions we can remove the open from the ViewModel.&lt;/p&gt;

&lt;p&gt;We then need to update our test file slightly, we want to use the default AndroidJUnit runner, and now we need to initialise mockito annotations manually&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@RunWith(AndroidJUnit4::class)
class ImageListScreenKtTest {
	@get:Rule
	val rule = createAndroidComposeRule&amp;lt;MainActivity&amp;gt;()

	@get:Rule
	val initRule: MockitoRule = MockitoJUnit.rule()
	...
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;we can then run our test successfully!&lt;/p&gt;
</description>
        <pubDate>Tue, 10 Nov 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/android-compose-testing/</link>
        <guid isPermaLink="true">http://g403.co/android-compose-testing/</guid>
        
        <category>JetPack Compose</category>
        
        <category>mockito</category>
        
        <category>Android</category>
        
        <category>Kotlin</category>
        
        
      </item>
    
      <item>
        <title>Securely inject API keys into your Android app during the build process</title>
        <description>&lt;p&gt;A bit of preamble, my gradle files are in kotlin, so my example code is similarly in kotlin rather than groovy&lt;/p&gt;

&lt;h2 id=&quot;pre-post-security-rant&quot;&gt;&lt;strong&gt;Pre-post Security Rant&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;When trying to figure out how to add api keys etc into an Android application I came across a lot of posts that used this as an example:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;getByName(&quot;release&quot;) {
  buildConfigField(&quot;String&quot;, &quot;SECURE_API_KEY&quot;, &quot;\&quot;SOMETHINGTHATSHOULDBESECUREPROD\&quot;&quot;)
}
getByName(&quot;staging&quot;) {
  buildConfigField(&quot;String&quot;, &quot;SECURE_API_KEY&quot;, &quot;\&quot;SOMETHINGTHATSHOULDBESECURESTAGE\&quot;&quot;)
}
getByName(&quot;debug&quot;) {
  buildConfigField(&quot;String&quot;, &quot;SECURE_API_KEY&quot;, &quot;\&quot;SOMETHINGTHATSHOULDBESECUREDEV\&quot;&quot;)
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Coming from a web dev fe/be background this makes me shake, you should &lt;strong&gt;never&lt;/strong&gt; have these keys in your repositories, &lt;strong&gt;always&lt;/strong&gt; inject them during build time. OK? OK.&lt;/p&gt;

&lt;h3 id=&quot;injecting-api-keys-from-environment-variables&quot;&gt;&lt;strong&gt;Injecting api keys from environment variables&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This actually took me a while to figure out, especially since I didn’t want to have to deal with environment variables when doing local development, it comes down to a few simple lines. During most builds we want to get it from the environment so we can simply do:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;android {
	compileSdkVersion(TARGET_SDK_VERSION)

	defaultConfig {
		minSdkVersion(MIN_SDK_VERSION)
		targetSdkVersion(TARGET_SDK_VERSION)

		testInstrumentationRunner(&quot;androidx.test.runner.AndroidJUnitRunner&quot;)
		consumerProguardFiles(&quot;consumer-rules.pro&quot;)
		buildConfigField(&quot;String&quot;, &quot;SECURE_API_KEY&quot;, &quot;\&quot;$System.getenv(&quot;SECURE_API_KEY&quot;)\&quot;&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Then during build time these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buildConfigField&lt;/code&gt; are built into a java class with static final’s in, this file also contains a few other things, here’s an example of what it’ll look like:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;package co.g403.android.apikeyblog;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean(&quot;false&quot;);
  public static final String APPLICATION_ID = &quot;co.g403.android.apikeyblog&quot;;
  public static final String BUILD_TYPE = &quot;release&quot;;
  public static final String SECURE_API_KEY = &quot;SOMETHINGTHATSHOULDBESECUREPROD&quot;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However for dev we want to do something slightly different. We don’t really want to mess around with environment variables.&lt;/p&gt;

&lt;h3 id=&quot;gradleproperties&quot;&gt;&lt;strong&gt;gradle.properties&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We can create a global gradle friendly file which can be read during build time too, if we create and save &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nano ~/.gradle/gradle.properties&lt;/code&gt; this will then also appear in our android projects and obviously wont be checked into version control, this will allow us to have different keys that a dev can use, and can be edited if we need to build different types without fear of accidentally checking the key into version control.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;myApiKey=SOMETHINGTHATSHOULDBESECUREDEV
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;we can then read this in our build.gradle(.kts)&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;buildTypes {
  getByName(&quot;debug&quot;) {
    val myApiKey: String by project
    buildConfigField(&quot;SECURE_API_KEY&quot;, myApiKey)
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and then when built our BuildConfig.java is updated&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;package co.g403.android.apikeyblog;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean(&quot;true&quot;);
  public static final String APPLICATION_ID = &quot;co.g403.android.apikeyblog&quot;;
  public static final String BUILD_TYPE = &quot;debug&quot;;
  // Field from build type: debug
  public static final String SECURE_API_KEY = &quot;SOMETHINGTHATSHOULDBESECUREDEV&quot;;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and that’s it! You now securely have API keys being injected during builds, with one that can easily be changed during development&lt;/p&gt;
</description>
        <pubDate>Tue, 20 Oct 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/android-environment-vars/</link>
        <guid isPermaLink="true">http://g403.co/android-environment-vars/</guid>
        
        <category>Gradle</category>
        
        <category>Android</category>
        
        <category>Kotlin</category>
        
        <category>Environment Variables</category>
        
        <category>buildConfigField</category>
        
        <category>BuildConfig</category>
        
        
      </item>
    
      <item>
        <title>Testing a Retrofit/RxJava Android library using Mockito</title>
        <description>&lt;h3 id=&quot;tests-in-android-say-what&quot;&gt;&lt;strong&gt;Tests in Android, say what?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;There’s lots of blog posts out there with great examples on how to do certain things in Kotlin/Android, take &lt;a href=&quot;/android-viewpager2/&quot;&gt;this one&lt;/a&gt; for example. What there seems to be a lack of, in general, is how to write tests. There’s some simple explanations about for classic “addition” unit testing, you know, you send 2 numbers into a function and get the sum of them out, about as trivial and far from the real world as possible (unless you’re writing node modules, then there’s probably already one for it). There are a few examples of tests for full apps, tapping buttons and things like that, but what I wanted to test was a simple library, something reusable.&lt;/p&gt;

&lt;p&gt;What I’ve been writing is a simple library for doing some network activity, going to flickr, fetching some images, all that jazz. We have a service interface that uses Retrofit 2 annotations and produces RxJava observables. We have a repository that takes the service as a constructor arg and uses the service to request some images, watches the observable and updates a Mutable/LiveData that an app/viewmodel/etc can observe for changes. This will get more complicated later involving pagination and updating etc, but this is the state of the library as I want to test it.&lt;/p&gt;

&lt;h3 id=&quot;repository-testing-and-mocked-network-calls&quot;&gt;&lt;strong&gt;Repository testing and mocked network calls&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Initially I want to test the repository, so that a known response from the api call produces consistent results to the client, the repository involves some transformation of the response, parsing/transforming it as needed, so sounds like a good candidate for our first set of tests. When writing js apps it’s most common to use axios for network traffic, and there’re many options for mocking that traffic during testing. Officially there’s moxios, mock adapter and unofficially network intercept apps like nock. Looking around Retrofit it has &lt;a href=&quot;https://github.com/square/Retrofit/tree/master/Retrofit-mock&quot;&gt;Retrofit-mock&lt;/a&gt; and so I started there. There wasn’t a lot of information out there, so I dove straight in and tried to see what I could make out, I ended up with a mockservice that produced network delegates and a brain that was streaming out through my ears, there had to be something better. I started to look at mockito to see if we could use that instead to mock our network traffic. This made the code a lot simpler, but I was still getting problems&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Expected :&amp;lt;[SizedImage(id=1)]&amp;gt;
Actual   :null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Google was not helping with this, as I said there was not many posts about testing and only a tiny subset of them had testing Retrofit and RxJava. Don’t get me wrong, millions of posts about Retrofit and RxJava, but who wants to write about testing in a blog post…&lt;/p&gt;

&lt;h3 id=&quot;immediate-executor-and-immediate-scheduler&quot;&gt;Immediate Executor and Immediate Scheduler&lt;/h3&gt;
&lt;p&gt;So I turned to my trusty source for all things Android, my sister Maja. Her response: “Immediate executor for tests, Google it”, and doing so led me to this led me to &lt;a href=&quot;https://www.codexpedia.com/android/unit-test-Retrofit-2-RxJava-2-and-livedata-in-android/&quot;&gt;this blog&lt;/a&gt; it seemed a bit out of date but was enough to get me to the end result I wanted. I added&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@Rule
@JvmField
var rule = InstantTaskExecutorRule()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and created a custom &lt;a href=&quot;https://github.com/gabriel403/AndroidTesting/blob/master/flickr/src/test/java/co/g403/android/flickr/datasource/RxImmediateSchedulerRule.kt&quot;&gt;scheduler rule&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Test rule for making the RxJava run synchronously in unit tests
companion object {
    @ClassRule
    @JvmField
    val schedulers = RxImmediateSchedulerRule()
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and then we were all working perfectly, I was able to assert against the responses as expected and get passing tests!
Amazing!&lt;/p&gt;

&lt;p&gt;You can view the code and tests as is in a very incomplete &lt;a href=&quot;https://github.com/gabriel403/AndroidTesting&quot;&gt;git repo&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Tue, 15 Sep 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/android-retrofit-unit-testing/</link>
        <guid isPermaLink="true">http://g403.co/android-retrofit-unit-testing/</guid>
        
        <category>Android</category>
        
        <category>Kotlin</category>
        
        <category>Mockito</category>
        
        <category>Retrofit</category>
        
        <category>RxJava</category>
        
        
      </item>
    
      <item>
        <title>AWS API Gateway part 2 - CORS 😱</title>
        <description>&lt;p&gt;After my &lt;a href=&quot;/aws-api-dynamodb&quot;&gt;last blog post&lt;/a&gt; we can start to make requests via axios to my API gateway. Hitting the gateway directly via postman or the browser worked fine, but with axios we were running into CORS issues. This is a super short blog detailing what we need to do to deal with these CORS issues. I’m not going to get into a big description about CORS because there’s a whole bunch of stuff about OPTIONS requests, origin headers and other boring stuff you can read elsewhere. We just want to know what to do to start making those axios requests right?&lt;/p&gt;

&lt;p&gt;A request can either be simple, or not simple, simple right? If a request is non-simple, you need to enable CORS.&lt;br /&gt;
A request is simple if all of these are true:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The API you’re accessing only allows GET, HEAD, and POST requests.&lt;/li&gt;
  &lt;li&gt;If it is a POST request, it must include an Origin header.&lt;/li&gt;
  &lt;li&gt;Content-type can only be text/plain, multipart/form-data, or application/x-www-form-urlencoded.&lt;/li&gt;
  &lt;li&gt;No contain custom headers.
&lt;a href=&quot;https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html&quot;&gt;See the AWS documention&lt;/a&gt;&lt;br /&gt;
Most API requests are going to be non-simple because we’re going to be making requests for application/json content, so we need to enable CORS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are 3 settings for this, all starting with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Access-Control-Allow-&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Headers&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Methods&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Origin&lt;/code&gt;. For developement we can just set all these to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt; and get on with poking some code around.&lt;br /&gt;
These go where we put the apigateway integration:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;x-amazon-apigateway-integration:
  credentials: !GetAtt [ApiGatewayRole, Arn]
  uri:
    'Fn::Sub': 'arn:aws:apigateway:${AWS::Region}:dynamodb:action/Scan'
  responses:
    default:
      statusCode: '200'
      responseParameters:
        method.response.header.Access-Control-Allow-Headers: &quot;'*'&quot;
        method.response.header.Access-Control-Allow-Methods: &quot;'*'&quot;
        method.response.header.Access-Control-Allow-Origin: &quot;'*'&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This will allow us to get up and running straight away, now to be more strict we want to specify what headers, methods and origins we’re allowing, what this actually does &lt;em&gt;I think&lt;/em&gt; is set the response headers from the OPTIONS request which tells the client what it can do on this API. So at a minimum I think this should get us up and running, we want to allow the content-type header, we want to allow options and get requests, and for now the traffic will come from our local dev&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        method.response.header.Access-Control-Allow-Headers: &quot;'Content-Type'&quot;
        method.response.header.Access-Control-Allow-Methods: &quot;'OPTIONS,GET'&quot;
        method.response.header.Access-Control-Allow-Origin: &quot;'http://localhost:3000'&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s an updated &lt;a href=&quot;/assets/aws-api-cors-template.yaml&quot;&gt;cloudformation-template.yml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that should be it! Enjoy!&lt;/p&gt;
</description>
        <pubDate>Sun, 19 Apr 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/aws-api-cors/</link>
        <guid isPermaLink="true">http://g403.co/aws-api-cors/</guid>
        
        <category>aws</category>
        
        <category>api</category>
        
        <category>api gateway</category>
        
        <category>cors</category>
        
        
      </item>
    
      <item>
        <title>Using AWS to create a dynamodb driven API</title>
        <description>&lt;p&gt;In the previous &lt;a href=&quot;/android-viewpager2&quot;&gt;post&lt;/a&gt; we created an Android app that displays some json data as a swipeable ViewPager, we stubbed the data for this app at the time. This time we’re going to be creating a dynamodb table driven API in AWS, cheap, simple and easy to maintain.&lt;/p&gt;

&lt;p&gt;This is going to be a very brief run through on setting up a cloudformation stack, I’m not going to cover setting up aws or aws-cli, in another blog post I’ll do this same project but with &lt;a href=&quot;https://aws.amazon.com/cdk/&quot;&gt;AWS CDK&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The basic pattern is we have an API gateway, and a dynamodb table, we keep the data we want in the table and the api directly fetches the data from the table.&lt;/p&gt;

&lt;h3 id=&quot;sam-cli&quot;&gt;&lt;strong&gt;SAM CLI&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We’re using SAM CLI for transforming our cloudformation template, so we need to ensure we have a line in the cloudformation template to make the transformation&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Transform: AWS::Serverless-2016-10-31
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;api-role&quot;&gt;&lt;strong&gt;API Role&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We need to create a role with appropriate policies to fetch the data from the table.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  ApiGatewayRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ['apigateway.amazonaws.com']
            Action: 'sts:AssumeRole'
      Path: '/'
      Policies:
        - PolicyName: DynamoApiPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'logs:*'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'dynamodb:Scan'
                  - 'dynamodb:Query'
                Resource:
                  'Fn::Sub': 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;dynamodb-table&quot;&gt;&lt;strong&gt;DynamoDB Table&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We create the table with an id we intend to use as a uuid, and then a secondary sorting id, and obviously we don’t need to specify the structure of the table.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Ref TableName
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
        - AttributeName: order_id
          AttributeType: N
      KeySchema:
        - AttributeName: id
          KeyType: HASH
        - AttributeName: order_id
          KeyType: RANGE
      ProvisionedThroughput:
        ReadCapacityUnits: '5'
        WriteCapacityUnits: '5'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;api-gateway&quot;&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;And then we create the API gateway to fetch the data from the table&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  ApiGatewayApi:
    Type: AWS::Serverless::Api
    DependsOn: DynamoDBTable
    Properties:
      Name: 'ServicesGateway'
      StageName: prod
      MethodSettings:
        - ResourcePath: '/*'
          HttpMethod: '*'
      DefinitionBody:
        openapi: '3.0.1'
        paths:
          /:
            get:
              responses:
                '200':
                  content:
                    application/json:
                      schema:
                        $ref: '#/components/schemas/Empty'
              x-amazon-apigateway-integration:
                credentials: !GetAtt [ApiGatewayRole, Arn]
                uri:
                  'Fn::Sub': 'arn:aws:apigateway:${AWS::Region}:dynamodb:action/Scan'
                responses:
                  default:
                    statusCode: '200'
                requestTemplates:
                  application/json:
                    { 'Fn::Sub': &quot;{\n  \&quot;TableName\&quot;: \&quot;${TableName}\&quot;\n}&quot; }
                passthroughBehavior: 'when_no_templates'
                httpMethod: 'POST'
                type: 'aws'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We use the scan action on the table to fetch all the data in a single request, this will cause problems if the dataset is large but will do just fine for the small datasets we’ll be using in the example.&lt;/p&gt;

&lt;h3 id=&quot;json-data&quot;&gt;&lt;strong&gt;JSON Data&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;There’s a bit more setup but this is essentially it, we insert the data we want to use.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;12340d21-5843-4935-92a7-d4c26fff1b21&quot;,
  &quot;order_id&quot;: 1,
  &quot;authorText&quot;: &quot;Pawełek Grzybek — Junior Developer&quot;,
  &quot;companyText&quot;: &quot;Mindera&quot;,
  &quot;testimonialText&quot;: &quot;Gabriel is an amazing developer and I'm so appreciative to have him as my Tech Lead, he's pretty too.&quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;json-response--transformation&quot;&gt;&lt;strong&gt;JSON Response &amp;amp; Transformation&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;When we make a request to fetch this we do get the dynamodb structure back in the response&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;Count&quot;: 1,
  &quot;Items&quot;: [
    {
      &quot;order_id&quot;: {
        &quot;N&quot;: &quot;1&quot;
      },
      &quot;testimonialText&quot;: {
        &quot;S&quot;: &quot;Gabriel is an amazing developer and I'm so appreciative to have him as my Tech Lead, he's pretty too.&quot;
      },
      &quot;id&quot;: {
        &quot;S&quot;: &quot;12340d21-5843-4935-92a7-d4c26fff1b21&quot;
      },
      &quot;companyText&quot;: {
        &quot;S&quot;: &quot;Mindera&quot;
      },
      &quot;authorText&quot;: {
        &quot;S&quot;: &quot;Pawełek Grzybek — Junior Developer&quot;
      }
    }
  ],
  &quot;ScannedCount&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;We can either transform this in the client calling this or we can add a response mapping transformation&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#set($inputRoot = $input.path('$'))
{
    &quot;testimonials&quot;: [
        #foreach($elem in $inputRoot.Items) {
            &quot;testimonialText&quot;: &quot;$elem.testimonialText.S&quot;,
            &quot;companyText&quot;: &quot;$elem.companyText.S&quot;,
            &quot;authorText&quot;: &quot;$elem.authorText.S&quot;
        }#if($foreach.hasNext),#end
	#end
    ]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This turns our template file from fairly generic into something very specific, but makes our client a lot cleaner.&lt;/p&gt;

&lt;p&gt;There’s some extra bits we can add in for a custom domain name, redundancy etc, otherwise we can access our api through the stand aws gateway address.&lt;/p&gt;

&lt;p&gt;The cloudformation template for this is suprisingly simple, barely a hundred lines of yaml, a bit too much to display in-line so here’s the final file &lt;a href=&quot;/assets/aws-api-dynamodb-template.yaml&quot;&gt;cloudformation-template.yaml&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;deploying-stack&quot;&gt;&lt;strong&gt;Deploying Stack&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We can run it with&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws cloudformation deploy --template-file template.yaml --stack-name api-db-testimonals-stack --capabilities CAPABILITY_IAM --region eu-west-2 --profile gabriel403 --parameter-overrides TableName=TestimonalsTable
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And that’s it! The API for our testimonial data is all setup!&lt;/p&gt;
</description>
        <pubDate>Tue, 14 Apr 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/aws-api-dynamodb/</link>
        <guid isPermaLink="true">http://g403.co/aws-api-dynamodb/</guid>
        
        <category>aws</category>
        
        <category>api</category>
        
        <category>dynamodb</category>
        
        <category>api gateway</category>
        
        
      </item>
    
      <item>
        <title>Implementing ViewPager2 in a basic Android app</title>
        <description>&lt;p&gt;I’ve only been learning Android for the last 6 months or so, in my spare time, so this is 1st in a series of quick blog posts on implementing some features in an Android app. These will be fairly basic for most experienced people, but helped me and maybe help other new devs with getting to grips with these things.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/gabriel403/ViewPager2Demo&quot;&gt;You can find the code in a GitHub repo&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;viewpager2&quot;&gt;ViewPager2&lt;/h1&gt;
&lt;p&gt;ViewPager allows us have a horizontal (and vertical) swiping component that populates from a series/list of various kinds, either Views, Fragements or data sources. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewPager2&lt;/code&gt; is built on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RecyclerView&lt;/code&gt;, which gives us some improvements around performance, rtl support, and it means when we’re familiar with one, we’re mostly familar with the other. We can also add fancy animations as we swipe.&lt;/p&gt;

&lt;p&gt;This is what we’ll be building
&lt;img src=&quot;/images/ViewPager2-demo.gif&quot; alt=&quot;Hardcore Swiping Action&quot; class=&quot;center-img&quot; width=&quot;200px&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;basics--intro&quot;&gt;Basics &amp;amp; intro&lt;/h2&gt;
&lt;p&gt;This is not by any means comprehensive, but is from my perspective, ViewModels retrieve data from repositories, fragments observe the ViewModel’s data and apply an adapter against them, an Adapter converts the data from the ViewModel into something that can be bound to the individual ViewHolders, and this is what updates the UI when we swipe. We use databinding to to do a lot of the heavy lifting for us.&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;

&lt;p&gt;We’re using the latest Android studio beta, but the latest stable Android studio should work fine. We create a new project with an empty activity to get started.&lt;/p&gt;

&lt;p&gt;First we want to enable data binding in the app&lt;/p&gt;

&lt;p&gt;Add this into the gradle file of your app module&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;buildFeatures {
  dataBinding = true
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;and we need to apply kapt plugin for databinding&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;apply plugin: &quot;kotlin-kapt&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and then further down we add a dependency on the viewpager2 module, we also include lifecycle-extensions to remove a few deprecated statements&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;implementation &quot;androidx.viewpager2:viewpager2:1.0.0&quot;
implementation &quot;androidx.lifecycle:lifecycle-extensions:2.2.0&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We then create a package to represent the data we want to use, in my example case testimonials, within that package a kotlin class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialAdapter&lt;/code&gt;, this will extend a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAdapter&lt;/code&gt; inorder to bind a testimonial data object to the layout, we’ll fill this out a bit more later.&lt;br /&gt;
Alongside this we create a package called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datasource&lt;/code&gt; and inside this a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Testimonial&lt;/code&gt; class and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialRespository&lt;/code&gt; class, these are fairly simple, and in the repository we’ll just return some pre-defined objects rather than getting bogged down in network or databse requests.&lt;/p&gt;

&lt;h2 id=&quot;data-data-data&quot;&gt;Data data data&lt;/h2&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;data class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;authorText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testimonialText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;companyText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testimonials&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MutableLiveData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;()&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getTestimonials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LiveData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;testimonials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testimonialText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Gabriel is an amazing developer and I'm so appreciative to have him as my Tech Lead, he's pretty too&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;authorText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Pawełek Grzybek — Junior Developer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;companyText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Mindera&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testimonialText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Gabs is one of the best at what he does, always available for a job, clean, methodical&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;authorText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Maja T — Przyjaciółka&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;companyText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Trzebiatowska Crime Family&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;testimonialText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;Gabriel is such a mediocre game player he makes me look good, I appreciate that he holds back so much for me&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;authorText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Ben R — bff&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;companyText&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Loser@Games Co.&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testimonials&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h3 id=&quot;infrastructure&quot;&gt;Infrastructure&lt;/h3&gt;
&lt;p&gt;Next we’re going to create a view to represent the testimonial and a fragement/viewmodel to hold them and give them some context.&lt;/p&gt;

&lt;p&gt;Create a new toplevel package ui, and within that a package home, and then create a new fragment (with viewmodel) within that.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;app/
    &lt;ul&gt;
      &lt;li&gt;testimonials/
        &lt;ul&gt;
          &lt;li&gt;TestimonialAdapter&lt;/li&gt;
          &lt;li&gt;datasource/
            &lt;ul&gt;
              &lt;li&gt;Testimonial&lt;/li&gt;
              &lt;li&gt;TestimonialRepository&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;ui/
        &lt;ul&gt;
          &lt;li&gt;home/
            &lt;ul&gt;
              &lt;li&gt;HomeFragment&lt;/li&gt;
              &lt;li&gt;HomeViewModel&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then within out layouts folder we want&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;layout
    &lt;ul&gt;
      &lt;li&gt;home_fragment&lt;/li&gt;
      &lt;li&gt;testimonials_layout&lt;/li&gt;
      &lt;li&gt;testimonial_layout&lt;/li&gt;
      &lt;li&gt;activity_main&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far we’ve covered the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Testimonial&lt;/code&gt; data class and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialRepository&lt;/code&gt; class, we’re going to switch focus for a bit and look at our layout files.&lt;/p&gt;

&lt;h2 id=&quot;layout-files&quot;&gt;Layout files&lt;/h2&gt;

&lt;p&gt;Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testimonial_layout&lt;/code&gt; is a simple file that binds with the Testimonial object uses TextViews to output the data contained within the individual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Testimonial&lt;/code&gt;, this is the layout that the ViewPager uses, and to which we bind the individual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Testimonial&lt;/code&gt; objects&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;layout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;xmlns:tools=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/tools&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;data&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;variable&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;item&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;co.g403.android.viewpager2demo.testimonials.datasource.Testimonial&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/data&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;LinearLayout&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonials_view_pager_container&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:orientation=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;vertical&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingStart=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;16dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingTop=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;40dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingEnd=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;16dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingBottom=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;40dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;tools:showIn=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@layout/home_fragment&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;TextView&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonial_text&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textAlignment=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;center&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textColor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#222222&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textSize=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;20sp&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@{item.testimonialText}&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tools:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Gabriel is cool&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;Space&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;20dp&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;TextView&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonial_author&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textAlignment=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;center&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textColor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#222222&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textSize=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;18sp&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@{item.authorText}&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tools:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Ben R — bff&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;Space&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;20dp&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;TextView&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonial_company&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textAlignment=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;center&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textColor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#444&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:textSize=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;16sp&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;android:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@{item.companyText}&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;tools:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Loser@Games Co.&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/LinearLayout&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/layout&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testimonials_layout&lt;/code&gt; is the container for our ViewPager2&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;LinearLayout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;xmlns:tools=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/tools&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:orientation=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;vertical&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:background=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#f5f5f5&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonials_layout&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;tools:showIn=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@layout/home_fragment&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;TextView&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/textView24&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:alpha=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0.9&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;People love me&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:textAlignment=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;center&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:textSize=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;24sp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;tools:text=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;People love me&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;androidx.viewpager2.widget.ViewPager2&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/testimonials_view_pager&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_weight=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/LinearLayout&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;home_fragement&lt;/code&gt; is a lot smaller and is just there as a wrapper around the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;testimonials_layout&lt;/code&gt;&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;layout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;xmlns:tools=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/tools&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ScrollView&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;tools:context=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;co.g403.android.viewpager2demo.MainActivity&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;include&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;layout=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@layout/testimonials_layout&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ScrollView&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/layout&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and we also need to update our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;activity_main&lt;/code&gt; layout to render our fragment, normally we’d be using the navigation or something, but this is just a simple app so we can do it like this&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;fragment&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/home_fragment&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;co.g403.android.viewpager2demo.ui.home.HomeFragment&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0dp&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:layout_weight=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;parent&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;parent&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;So we’ve covered the layouts, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Testimonial&lt;/code&gt; data class and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialRepository&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;app/
    &lt;ul&gt;
      &lt;li&gt;testimonials/
        &lt;ul&gt;
          &lt;li&gt;TestimonialAdapter&lt;/li&gt;
          &lt;li&gt;datasource/
            &lt;ul&gt;
              &lt;li&gt;Testimonial ✅&lt;/li&gt;
              &lt;li&gt;TestimonialRepository ✅&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;ui/
        &lt;ul&gt;
          &lt;li&gt;home/
            &lt;ul&gt;
              &lt;li&gt;HomeFragment&lt;/li&gt;
              &lt;li&gt;HomeViewModel&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;layout
    &lt;ul&gt;
      &lt;li&gt;home_fragment ✅&lt;/li&gt;
      &lt;li&gt;testimonials_layout ✅&lt;/li&gt;
      &lt;li&gt;testimonial_layout ✅&lt;/li&gt;
      &lt;li&gt;activity_main ✅&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now to wrap up the rest of the binding of the data to the layout, starting with the HomeViewModel, this just gets the repository of testimonials and stores them to be used in the layout&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testimonialRepository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testimonials&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testimonialRepository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTestimonials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now we’re down to the couple of classes that do most of the heavy lifting, the adapter and the fragment&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialAdapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ListAdapter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;TestimonialItemDiffCallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onCreateViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialViewHolder&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;layoutInflater&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LayoutInflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;binding&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DataBindingUtil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ViewDataBinding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layoutInflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onBindViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;holder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;holder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getItemViewType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testimonial_layout&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialItemDiffCallback&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DiffUtil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ItemCallback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;areItemsTheSame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldItem&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;areContentsTheSame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldItem&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewDataBinding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RecyclerView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;ViewHolder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;setVariable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;BR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testimonial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;executePendingBindings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;We create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialAdapter&lt;/code&gt; that extends the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAdapter&lt;/code&gt;, this already gives us a few things to make development easier, but we do have to implement our own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DiffUtil.ItemCallback&lt;/code&gt; for it. The override of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onCreateViewHolder&lt;/code&gt; is so we can use databinding to bind the the data into the viewholder, our override of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onBindViewHolder&lt;/code&gt; is so we can bind the correct testimonial to the viewholder and we override &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getItemViewType&lt;/code&gt; so that list/viewholder knows what layout is needed to inflate/populate.&lt;br /&gt;
The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialViewHolder&lt;/code&gt; takes in the data binding and when the adapter passes the testimonial to the viewholder we trigger the binding to update the variable in the layout.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-lang=&quot;kotlin&quot;&gt;&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeFragment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HomeViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;testimonialsAdapter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialAdapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestimonialAdapter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onCreateView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;inflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LayoutInflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;View&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;inflater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inflate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;home_fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onViewCreated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;View&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onViewCreated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testimonials_view_pager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;adapter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;testimonialsAdapter&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;onActivityCreated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Bundle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onActivityCreated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;savedInstanceState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;testimonials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;observe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewLifecycleOwner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Observer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;testimonialsAdapter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submitList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Here we have the final piece of the app, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HomeFragment&lt;/code&gt;, here we hold the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HomeViewModel&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TestimonialAdapter&lt;/code&gt;, when we create the view (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onViewCreated&lt;/code&gt;) we set up all our binding mechanics.&lt;br /&gt;
Then in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onActivityCreated&lt;/code&gt; we observe the LiveData List of Testimonials that’s in the ViewModel and when it changes we apply the TestimonialAdapter to it, so that it’s able to bind correctly to our view&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;app/
    &lt;ul&gt;
      &lt;li&gt;testimonials/
        &lt;ul&gt;
          &lt;li&gt;TestimonialAdapter ✅&lt;/li&gt;
          &lt;li&gt;datasource/
            &lt;ul&gt;
              &lt;li&gt;Testimonial ✅&lt;/li&gt;
              &lt;li&gt;TestimonialRepository ✅&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;ui/
        &lt;ul&gt;
          &lt;li&gt;home/
            &lt;ul&gt;
              &lt;li&gt;HomeFragment ✅&lt;/li&gt;
              &lt;li&gt;HomeViewModel ✅&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;layout
    &lt;ul&gt;
      &lt;li&gt;home_fragment ✅&lt;/li&gt;
      &lt;li&gt;testimonials_layout ✅&lt;/li&gt;
      &lt;li&gt;testimonial_layout ✅&lt;/li&gt;
      &lt;li&gt;activity_main ✅&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that it’s complete, once we build and run this we’ll get an app with a swipeable section as you can see in the gif below!&lt;br /&gt;
&lt;a href=&quot;https://github.com/gabriel403/ViewPager2Demo&quot;&gt;You can find the code in a GitHub repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m very concsious that we’ve skipped any form of testing for this, I don’t know much about testing in Android, but I have the creepy feeling of my bestie, Maja, breathing down my neck at the lack of them.&lt;/p&gt;

&lt;p&gt;Next I’ll be writing:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;writing tests for ViewPager&lt;/li&gt;
  &lt;li&gt;a quick post about repositories&lt;/li&gt;
  &lt;li&gt;maybe one on different animations in ViewPager&lt;/li&gt;
  &lt;li&gt;Annotations and BindingAdapaters (glide, extra formatting or styling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/ViewPager2-demo.gif&quot; alt=&quot;Hardcore Swiping Action&quot; class=&quot;center-img&quot; width=&quot;200px&quot; /&gt;&lt;/p&gt;

</description>
        <pubDate>Mon, 30 Mar 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/android-viewpager2/</link>
        <guid isPermaLink="true">http://g403.co/android-viewpager2/</guid>
        
        <category>android</category>
        
        <category>kotlin</category>
        
        <category>viewpager</category>
        
        <category>databinding</category>
        
        
      </item>
    
      <item>
        <title>Using GitHub actions to deploy branch builds to a s3 bucket</title>
        <description>&lt;p&gt;My first blog post in FIVE YEARS :open_mouth:&lt;/p&gt;

&lt;p&gt;One of the problems you get with working on side projects with a group of people is the choices you have to make around tooling. You can use one of the awesome hosting providers like netlify or now.sh, but you’re normally limited in the number of people that can join the project to contribute and/or the number of “build minutes” your project can do each month.&lt;/p&gt;

&lt;p&gt;For one of the projects I’m involved in we’re using a private &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; repo (which will become public when we’re ready) and &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt; for the initial hosting.
Netlify on the free tier limits you to 200 build minutes/month and 2 collaborators. I decided to investigate how we could use &lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub actions&lt;/a&gt; (limited to 2k minutes/month) and an &lt;a href=&quot;https://aws.amazon.com/s3/pricing/&quot;&gt;s3 bucket&lt;/a&gt; for which the pricing is pretty trivial, to see if this is a viable alternative. And for something to blog about.&lt;/p&gt;

&lt;h3 id=&quot;github-actions&quot;&gt;GitHub actions&lt;/h3&gt;

&lt;p&gt;Like many code hosting apps GitHub now offers the ability to run a pipeline for linting/building/testing/deploying code. These actions can be filtered based on pushing to branch, opening pull requests, making a comment etc, and there are many publicly available pre-built actions that we can use, but we also have the option to write/run our own.&lt;/p&gt;

&lt;h3 id=&quot;website-hosting-with-s3&quot;&gt;Website hosting with S3&lt;/h3&gt;

&lt;p&gt;I’m not going to go over the instructions for setting this up, for once the AWS documentation has pretty good coverage for this &lt;a href=&quot;https://docs.aws.amazon.com/AmazonS3/latest/dev/HowDoIWebsiteConfiguration.html&quot;&gt;AWS Docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For one of the GitHub actions we use for this we do need to do some more manual setup, for the IAM user needed for pushing to the bucket, this is specifically for Gatsby which our project is in &lt;a href=&quot;https://github.com/jonelantha/gatsby-s3-action#notes&quot;&gt;jonelantha/gatsby-s3-action&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;/h3&gt;

&lt;p&gt;For putting it together I created a new &lt;a href=&quot;https://github.com/gabriel403/branch-builds-into-s3&quot;&gt;GH repo&lt;/a&gt;, created a new bucket in &lt;a href=&quot;http://gh-actions-branch-builds.s3-website-eu-west-1.amazonaws.com&quot;&gt;S3&lt;/a&gt; and setup the website hosting and IAM user as detailed above.&lt;/p&gt;

&lt;p&gt;I then added Gatsby to the repo and pushed that up, and finally started work on the GH actions&lt;/p&gt;

&lt;h3 id=&quot;lights-cameras--git-branch-name&quot;&gt;lights, cameras, … git branch name?&lt;/h3&gt;

&lt;p&gt;I was quite easily able to get the repo building and syncing to s3:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;name: Deploy to S3

on: [push]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Use Node.js 12
        uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Build
        run: |
          yarn
          yarn build
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${ { secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${ { secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-1
      - name: Deploy
        uses: jonelantha/gatsby-s3-action@v1
        with:
          dest-s3-bucket: gh-actions-branch-builds

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But all this was doing was pushing the code into the root of the bucket, so everytime a new commit was pushed on any branch it would overwrite what was currently deployed. What I needed to do was push code to a subdir based on the branch name.&lt;/p&gt;

&lt;p&gt;GitHub actions provide various environment variables that can be used in scripts, one of these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GITHUB_REF&lt;/code&gt; in branches will contain the branch name, so we’re able to extract the branch name from this ref, there are various caveats to this, but for this simple case &lt;a href=&quot;https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions&quot;&gt;Stack Overflow has my back&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can plug in a couple of extra steps to get the branch name and add it to the gatsby config and then update the gatsby-s3-action to include the branch name and update the gatsby build step to include prefix path&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;      - name: Extract branch name
        shell: bash
        run: echo &quot;##[set-output name=branch_name;]$(echo ${GITHUB_REF#refs/heads/})&quot;
        id: extract_branch
      - name: Find and Replace
        uses: jacobtomlinson/gha-find-replace@master
        with:
          find: 'PATH_PREFIX'
          replace: ${ { steps.extract_branch.outputs.branch_name } }
          include: gatsby-config.js
...
      - name: Build
        run: |
          yarn
          yarn build --prefix-paths
...
      - name: Deploy
        uses: jonelantha/gatsby-s3-action@v1
        with:
          dest-s3-bucket: gh-actions-branch-builds/${ { steps.extract_branch.outputs.branch_name }}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now whenever we push a new branch with changes in this will create a subdirectory in our bucket and the whole thing works amazingly well! :tada:&lt;/p&gt;

&lt;p&gt;I’ve put together a repo for the demo:&lt;br /&gt;
&lt;a href=&quot;https://github.com/gabriel403/branch-builds-into-s3&quot;&gt;https://github.com/gabriel403/branch-builds-into-s3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And setup a couple of branch builds as examples:&lt;br /&gt;
&lt;a href=&quot;http://gh-actions-branch-builds.s3-website-eu-west-1.amazonaws.com/master/&quot;&gt;http://gh-actions-branch-builds.s3-website-eu-west-1.amazonaws.com/master/&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;http://gh-actions-branch-builds.s3-website-eu-west-1.amazonaws.com/haxing-update/&quot;&gt;http://gh-actions-branch-builds.s3-website-eu-west-1.amazonaws.com/haxing-update/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there we have it! I haven’t had a chance to look into deleting directories when PRs are merged, and I know opening a PR will put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GITHUB_REF&lt;/code&gt; into a weird state, but there are different env vars we can use.&lt;/p&gt;

&lt;p&gt;Hope this helps some folks!&lt;/p&gt;

&lt;p&gt;Much love :heart:&lt;/p&gt;
</description>
        <pubDate>Sun, 01 Mar 2020 00:00:00 +0000</pubDate>
        <link>http://g403.co/github-actions-and-branch-builds/</link>
        <guid isPermaLink="true">http://g403.co/github-actions-and-branch-builds/</guid>
        
        <category>GitHub</category>
        
        <category>s3</category>
        
        <category>GitHub actions</category>
        
        <category>Branch builds</category>
        
        
      </item>
    
      <item>
        <title>Converting Rails Apps From Heroku To Docker</title>
        <description>&lt;p&gt;We recently had to move a rails app from heroku+postgres to our own servers, since heroku free is now enforcing nap times (but I don’t wanna nap!).
We don’t want to have to manage the rails dependencies ourselves, as they can be a pita. &lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker&lt;/a&gt; to the rescue!&lt;/p&gt;

&lt;p&gt;We need to run 2 containers for this, a postgres container for data storage and the actual rails app. We need to persist our postgres data so we mount a local dir into the postgres container and then link from the app container to the postgres container.&lt;/p&gt;

&lt;p&gt;We have a structure similar to:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;-magicapp
--repo
--postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;##RoR docker container
As recommended by the &lt;a href=&quot;https://hub.docker.com/_/rails/&quot;&gt;rails docker page&lt;/a&gt; we build our own Dockerfile based on their onbuild Dockerfile.&lt;/p&gt;

&lt;p&gt;The onbuild Dockerfile builds in some weird places, so it’s customised a bit to build in the more traditional (from docker perspective) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/app&lt;/code&gt; folder as a mount point.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM ruby:2.1

# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y nodejs --no-install-recommends &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y mysql-client postgresql-client sqlite3 --no-install-recommends &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

ENV RAILS_ENV production

RUN mkdir /app
VOLUME /app
WORKDIR /app
COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN bundle install

EXPOSE 3000
CMD [&quot;rails&quot;, &quot;server&quot;, &quot;-b&quot;, &quot;0.0.0.0&quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We use the normal rails server at this point but can easily be replaced with unicorn or whatever.
Now we can build the docker image (and push to docker hub if wanted).&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker build -t &quot;gabriel403/magicapp&quot;
docker push &quot;gabriel403/magicapp&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;##Postgres docker container
So now we have a workable docker image for out rails app (with gems pre-bundled thanks to building it!) we want to get a working postgres running for us to store data in! We supply a couple of env variables to create a db/user and password. This one’s fairly simple thanks to the existing &lt;a href=&quot;https://hub.docker.com/_/postgres/&quot;&gt;postgres image&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run --name magicapp-postgres -d -e POSTGRES_USER=magicapp_prod_user -e POSTGRES_DB=magicapp_prod_db -e POSTGRES_PASSWORD=magicapp_prod_pword -v /var/www/vhosts/magicapp/postgres:/var/lib/postgresql/data postgres
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we come to launch our app container the env variables from our postgres container are exposed inside the app container, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POSTGRES_USER&lt;/code&gt; becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POSTGRES_ENV_POSTGRES_USER&lt;/code&gt;, but they don’t exist outside of the containers, making it awkward to pass in indirectly. We can alter our app so the database.yml references the new env vars:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;production:
  &amp;lt;&amp;lt;: *default
  adapter: postgresql
  database: &amp;lt;%= ENV['POSTGRES_ENV_POSTGRES_DB'] %&amp;gt;
  username: &amp;lt;%= ENV['POSTGRES_ENV_POSTGRES_USER'] %&amp;gt;
  password: &amp;lt;%= ENV['POSTGRES_ENV_POSTGRES_PASSWORD'] %&amp;gt;
  host: &amp;lt;%= ENV['POSTGRES_PORT_5432_TCP_ADDR'] %&amp;gt;
  port: &amp;lt;%= ENV['POSTGRES_PORT_5432_TCP_PORT'] %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or we can manually pass the details to more standard env vars:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;-e POSTGRES_USER=magicapp_prod_user -e POSTGRES_DB=magicapp_prod_db -e POSTGRES_PASSWORD=magicapp_prod_pword
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;it’s up to you to chose which approach you prefer, but we do pass a couple of env vars in for basic rails operation:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run --link magicapp-postgres:postgres --name magicapp -d -e LANG=en_GB.UTF-8 -e RAIL_ENV=production -e SECRET_KEY_BASE=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab -v /var/www/vhosts/magicapp/repo:/app gabriel403/magicapp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And there we have it, we have a rails app working on docker connecting to another docker container which is running postgres.&lt;/p&gt;

&lt;p style=&quot;padding-top: 400px;&quot;&gt;&lt;/p&gt;

&lt;p&gt;##Import postgres backup
Nah that’s not it don’t worry. You’re probably sat there thinking that’s no good, what about the years of data we’ve already got in the heroku db? What about proxying from http[s] to the rails app? Don’t worry I got you covered.&lt;/p&gt;

&lt;p&gt;If you’re using heroku’s postgres you should be able to make a backup of the current db and then download it, we can then transfer that to the docker host and import to our postgres db.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/converting-rails/addon.png&quot; alt=&quot;Postgres addon&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/converting-rails/pg-backup.png&quot; alt=&quot;Postgres backup&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/converting-rails/pg-backup-download.png&quot; alt=&quot;Postgres backup download&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now we can download the backup we’ve just made. When downloaded it’ll have a name like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;46213451-51f0-490a-311d-22c24ea1e4f5&lt;/code&gt;, we rename it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;magicapp-dump.dump&lt;/code&gt; and then upload it to a temporary directory on the docker host &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/www/vhosts/magicapp/tmp&lt;/code&gt; in our case.&lt;/p&gt;

&lt;p&gt;Now we can use the default postgres docker image to import the dump into our existing postgres db, we’ll be prompted on the command line to enter the password for the user:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run --rm -it --link magicapp-postgres:postgres -v /var/www/vhosts/magicapp/tmp:/tmp postgres sh -c 'exec pg_restore --verbose --clean --no-acl --no-owner -h &quot;$POSTGRES_PORT_5432_TCP_ADDR&quot; -p &quot;$POSTGRES_PORT_5432_TCP_PORT&quot; -U &quot;$POSTGRES_ENV_POSTGRES_USER&quot; -d &quot;$POSTGRES_ENV_POSTGRES_DB&quot; /tmp/magicapp-dump.dump'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now you’ve got your data restored to your new postgres container!&lt;/p&gt;

&lt;p&gt;You’ll probably see something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WARNING: errors ignored on restore: 20&lt;/code&gt; these are normally todo with the dump attempting to drop tables that don’t yet exist.&lt;/p&gt;

&lt;p&gt;##http proxy container
So we’ve now got an app running with a db with our restored data in, time for some http[s] proxy magic. We’re not using one of the docker special images for this, but a rather awesome image from jwilder called &lt;a href=&quot;https://hub.docker.com/r/jwilder/nginx-proxy/&quot;&gt;nginx-proxy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One of the beauties about docker is it it’s easy to see what the user has done to the image before we use it, just by looking at the Dockerfile. One of the things that’s great about this docker image is that it can handle most of your proxying without fuss, you don’t need new containers for each url you wish to proxy. We want to proxy both http as well as https traffic (the rails app will redirect to https for us) we need to tell it where our ssl certificates are, and to forward the appropriate ports, but other than that it’s fairly simple to get running.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run -d --name nginx-proxy -p 80:80 -p 443:443 -v /var/www/vhosts/ssl/g403.co:/etc/nginx/certs -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we’re using this we actually need to start our rails app with a few more env vars that the proxy container uses to channel.&lt;/p&gt;

&lt;p&gt;So instead of&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run --link magicapp-postgres:postgres --name magicapp -d -e LANG=en_GB.UTF-8 -e RAIL_ENV=production -e SECRET_KEY_BASE=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab -v /var/www/vhosts/magicapp/repo:/app gabriel403/magicapp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We now need&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run --link magicapp-postgres:postgres --name magicapp -d -e CERT_NAME=g403.co -e VIRTUAL_HOST=magicapp.g403.co -e VIRTUAL_PORT=3000 -e LANG=en_GB.UTF-8 -e RAIL_ENV=production -e SECRET_KEY_BASE=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab -v /var/www/vhosts/magicapp/repo:/app gabriel403/magicapp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So now we have a running RoR container, a running Postgres container with data restored from heroku and finally an nginx proxy to forward traffic from normal http/s ports to our app.&lt;/p&gt;

&lt;p&gt;##systemd configuration
Whilst docker is happy to run on boot, our container’s won’t boot or restart by themselves. These are a basic set of systemd files to get all 3 containers to run. Simply put them in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/systemd/system/&lt;/code&gt; and then run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo systemctl enable docker-nginx-proxy.service&lt;/code&gt; etc to enable them and get them to start on boot/restart on error etc.&lt;/p&gt;

&lt;p&gt;docker-nginx-proxy.service&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=Nginx Proxy Container
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStartPre=service docker restart
ExecStart=/usr/bin/docker run --name nginx-proxy -p 80:80 -p 443:443 -v /var/www/vhosts/ssl/g403.co:/etc/nginx/certs -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
ExecStop=/usr/bin/docker stop -t 2 nginx-proxy ; /usr/bin/docker rm -f nginx-proxy

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;docker-magicapp.service&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=Magicapp Container
Requires=docker.service
After=docker.service
Requires=docker-magicapp-postgres.service
After=docker-magicapp-postgres.service

[Service]
Restart=always
ExecStart=/usr/bin/docker run --name magicapp --link magicapp-postgres:postgres -e CERT_NAME=g403.co -e VIRTUAL_HOST=magicapp.g403.co -e VIRTUAL_PORT=3000 -e LANG=en_GB.UTF-8 -e RAIL_ENV=production -e SECRET_KEY_BASE=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab -v /var/www/vhosts/magicapp/repo:/app gabriel403/magicapp
ExecStop=/usr/bin/docker stop -t 2 magicapp ; /usr/bin/docker rm -f magicapp

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;docker-magicapp-postgres.service&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Unit]
Description=Magicapp Postgres Container
Requires=docker.service
After=docker.service

[Service]
Restart=always
ExecStart=/usr/bin/docker run --name magicapp-postgres -e POSTGRES_USER=magicapp_prod_user -e POSTGRES_DB=magicapp_prod_db -e POSTGRES_PASSWORD=magicapp_prod_pword -v /var/www/vhosts/magicapp/postgres:/var/lib/postgresql/data postgres

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note that we don’t pass the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-d&lt;/code&gt; flag in the run commands, we don’t want to background the running, as this would cause systemd to continuously restart.&lt;/p&gt;
</description>
        <pubDate>Wed, 16 Sep 2015 00:00:00 +0000</pubDate>
        <link>http://g403.co/converting-rails-app-from-heroku-to-docker/</link>
        <guid isPermaLink="true">http://g403.co/converting-rails-app-from-heroku-to-docker/</guid>
        
        <category>docker</category>
        
        <category>rails</category>
        
        <category>heroku</category>
        
        
      </item>
    
      <item>
        <title>A Howto on Converting SVN repos to Git</title>
        <description>&lt;p&gt;One of our major projects at GoMAD Tech is currently stored in SVN but is soon moving to git.&lt;/p&gt;

&lt;p&gt;It really consists of multiple smaller projects, however they’re all stored in a single SVN repository. With SVN’s subdirectory access this was never a problem, but when parsed into Git these need to be seperate repositories.&lt;/p&gt;

&lt;p&gt;We also need to remove unnecessary branches, we have a standard layout of trunk, branches/development and branches/hotfix branches with trunk becoming the master branch. As well as a whole bunch of tags in the tags directory, these need to become normal git tags.&lt;/p&gt;

&lt;p&gt;One of the things we need to remember is to adjust the author names and emails for commit messages. So we need to pull the commit authors out of svn and tweak them.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;svn log --xml https://svn.gomadtech.com/svn/project/subdir/projectdir | grep -P &quot;^&amp;lt;author&quot; | sort -u | perl -pe 's/&amp;lt;author&amp;gt;(.*?)&amp;lt;\\\/author&amp;gt;/$1 = /' &amp;gt; users.txt we can then edit this file to make the entries look like this  

gabriel.baker = Gabriel Baker &amp;lt;gabriel.baker@gomadtech.com&amp;gt;
shabbir.hassanally = Shabbir Hassanally &amp;lt;shabbir.hassanally@gomadtech.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After fixing the authors we can start reading out the svn commits and parsing them into git ones, lucky git has this built in (remember to install git and git svn)&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git-svn clone https://svn.gomadtech.com/svn/project/subdir/projectdir --authors-file=users.txt --no-metadata -s ProjectInGit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So now we have a local repo with history still intact, but we’re missing proper gittags and branches.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gabriel@svn:~/ProjectInGit$ git status
# On branch master
nothing to commit (working directory clean)
gabriel@svn:~/ProjectInGit$ git branch
* master
gabriel@svn:~/ProjectInGit$ git tag
gabriel@svn:~/ProjectInGit$ So we need to parse those out and create first our tags:

git for-each-ref refs/remotes/tags | cut -d / -f 4- | grep -v @ | while read tagname; do git tag &quot;$tagname&quot; &quot;tags/$tagname&quot;; git branch -r -d &quot;tags/$tagname&quot;; done
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And we have:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Deleted remote branch tags/2.0.0 (was 7dea8c7).
Deleted remote branch tags/2.1.0 (was 5b7a0cc).
Deleted remote branch tags/2.1.1 (was a1f7702).
gabriel@svn:~/ProjectInGit$ git branch
* master
gabriel@svn:~/ProjectInGit$ git tag
2.0.0
2.1.0
2.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then we create our branches:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git for-each-ref refs/remotes | cut -d / -f 3- | grep -v @ | while read branchname; do git branch &quot;$branchname&quot; &quot;refs/remotes/$branchname&quot;; git branch -r -d &quot;$branchname&quot;; done

Deleted remote branch development (was 28cd707).
Deleted remote branch hotfixes (was 81b7b4b).
Deleted remote branch trunk (was fe2b690).
gabriel@svn:~/ProjectInGit$ git branch
  development
  hotfixes
* master
  trunk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No point keeping the trunk branch now we have master&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gabriel@svn:~/ProjectInGit$ git branch -D trunk
Deleted branch trunk (was fe2b690).
gabriel@svn:~/ProjectInGit$ git branch
  development
  hotfixes
* master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we can add our new remote and push our branches and tags to it&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git remote add origin git@github.com:GoMADTech/Project.git
git push origin --all
git push origin --tags
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And now we have you repo, converted from SVN to Git and hosted on Github.
Enjoy!&lt;/p&gt;
</description>
        <pubDate>Sun, 22 Sep 2013 00:00:00 +0000</pubDate>
        <link>http://g403.co/converting-svn-to-git/</link>
        <guid isPermaLink="true">http://g403.co/converting-svn-to-git/</guid>
        
        <category>svn</category>
        
        <category>git</category>
        
        
      </item>
    
  </channel>
</rss>
